969 lines
27 KiB
Python
969 lines
27 KiB
Python
# -*- coding:utf-8 -*-
|
|
|
|
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software Foundation,
|
|
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
|
|
#
|
|
# ##### END GPL LICENSE BLOCK #####
|
|
|
|
# <pep8 compliant>
|
|
|
|
# ----------------------------------------------------------
|
|
# Author: Stephen Leger (s-leger)
|
|
#
|
|
# ----------------------------------------------------------
|
|
from mathutils import Vector, Matrix
|
|
from math import sin, cos, pi, atan2, sqrt, acos
|
|
import bpy
|
|
# allow to draw parts with gl for debug puropses
|
|
from .archipack_gl import GlBaseLine
|
|
|
|
|
|
class Projection(GlBaseLine):
|
|
|
|
def __init__(self):
|
|
GlBaseLine.__init__(self)
|
|
|
|
def proj_xy(self, t, next=None):
|
|
"""
|
|
length of projection of sections at crossing line / circle intersections
|
|
deformation unit vector for profil in xy axis
|
|
so f(x_profile) = position of point in xy plane
|
|
"""
|
|
if next is None:
|
|
return self.normal(t).v.normalized(), 1
|
|
v0 = self.normal(1).v.normalized()
|
|
v1 = next.normal(0).v.normalized()
|
|
direction = v0 + v1
|
|
adj = (v0 * self.length) * (v1 * next.length)
|
|
hyp = (self.length * next.length)
|
|
c = min(1, max(-1, adj / hyp))
|
|
size = 1 / cos(0.5 * acos(c))
|
|
return direction.normalized(), min(3, size)
|
|
|
|
def proj_z(self, t, dz0, next=None, dz1=0):
|
|
"""
|
|
length of projection along crossing line / circle
|
|
deformation unit vector for profil in z axis at line / line intersection
|
|
so f(y) = position of point in yz plane
|
|
"""
|
|
return Vector((0, 1)), 1
|
|
"""
|
|
NOTE (to myself):
|
|
In theory this is how it has to be done so sections follow path,
|
|
but in real world results are better when sections are z-up.
|
|
So return a dumb 1 so f(y) = y
|
|
"""
|
|
if next is None:
|
|
dz = dz0 / self.length
|
|
else:
|
|
dz = (dz1 + dz0) / (self.length + next.length)
|
|
return Vector((0, 1)), sqrt(1 + dz * dz)
|
|
# 1 / sqrt(1 + (dz0 / self.length) * (dz0 / self.length))
|
|
if next is None:
|
|
return Vector((-dz0, self.length)).normalized(), 1
|
|
v0 = Vector((self.length, dz0))
|
|
v1 = Vector((next.length, dz1))
|
|
direction = Vector((-dz0, self.length)).normalized() + Vector((-dz1, next.length)).normalized()
|
|
adj = v0 * v1
|
|
hyp = (v0.length * v1.length)
|
|
c = min(1, max(-1, adj / hyp))
|
|
size = -cos(pi - 0.5 * acos(c))
|
|
return direction.normalized(), size
|
|
|
|
|
|
class Line(Projection):
|
|
"""
|
|
2d Line
|
|
Internally stored as p: origin and v:size and direction
|
|
moving p will move both ends of line
|
|
moving p0 or p1 move only one end of line
|
|
p1
|
|
^
|
|
| v
|
|
p0 == p
|
|
"""
|
|
def __init__(self, p=None, v=None, p0=None, p1=None):
|
|
"""
|
|
Init by either
|
|
p: Vector or tuple origin
|
|
v: Vector or tuple size and direction
|
|
or
|
|
p0: Vector or tuple 1 point location
|
|
p1: Vector or tuple 2 point location
|
|
Will convert any into Vector 2d
|
|
both optionnals
|
|
"""
|
|
Projection.__init__(self)
|
|
if p is not None and v is not None:
|
|
self.p = Vector(p).to_2d()
|
|
self.v = Vector(v).to_2d()
|
|
elif p0 is not None and p1 is not None:
|
|
self.p = Vector(p0).to_2d()
|
|
self.v = Vector(p1).to_2d() - self.p
|
|
else:
|
|
self.p = Vector((0, 0))
|
|
self.v = Vector((0, 0))
|
|
self.line = None
|
|
|
|
@property
|
|
def copy(self):
|
|
return Line(self.p.copy(), self.v.copy())
|
|
|
|
@property
|
|
def p0(self):
|
|
return self.p
|
|
|
|
@property
|
|
def p1(self):
|
|
return self.p + self.v
|
|
|
|
@p0.setter
|
|
def p0(self, p0):
|
|
"""
|
|
Note: setting p0
|
|
move p0 only
|
|
"""
|
|
p1 = self.p1
|
|
self.p = Vector(p0).to_2d()
|
|
self.v = p1 - p0
|
|
|
|
@p1.setter
|
|
def p1(self, p1):
|
|
"""
|
|
Note: setting p1
|
|
move p1 only
|
|
"""
|
|
self.v = Vector(p1).to_2d() - self.p
|
|
|
|
@property
|
|
def length(self):
|
|
"""
|
|
3d length
|
|
"""
|
|
return self.v.length
|
|
|
|
@property
|
|
def angle(self):
|
|
"""
|
|
2d angle on xy plane
|
|
"""
|
|
return atan2(self.v.y, self.v.x)
|
|
|
|
@property
|
|
def a0(self):
|
|
return self.angle
|
|
|
|
@property
|
|
def angle_normal(self):
|
|
"""
|
|
2d angle of perpendicular
|
|
lie on the right side
|
|
p1
|
|
|--x
|
|
p0
|
|
"""
|
|
return atan2(-self.v.x, self.v.y)
|
|
|
|
@property
|
|
def reversed(self):
|
|
return Line(self.p, -self.v)
|
|
|
|
@property
|
|
def oposite(self):
|
|
return Line(self.p + self.v, -self.v)
|
|
|
|
@property
|
|
def cross_z(self):
|
|
"""
|
|
2d Vector perpendicular on plane xy
|
|
lie on the right side
|
|
p1
|
|
|--x
|
|
p0
|
|
"""
|
|
return Vector((self.v.y, -self.v.x))
|
|
|
|
@property
|
|
def cross(self):
|
|
return Vector((self.v.y, -self.v.x))
|
|
|
|
def signed_angle(self, u, v):
|
|
"""
|
|
signed angle between two vectors range [-pi, pi]
|
|
"""
|
|
return atan2(u.x * v.y - u.y * v.x, u.x * v.x + u.y * v.y)
|
|
|
|
def delta_angle(self, last):
|
|
"""
|
|
signed delta angle between end of line and start of this one
|
|
this value is object's a0 for segment = self
|
|
"""
|
|
if last is None:
|
|
return self.angle
|
|
return self.signed_angle(last.straight(1, 1).v, self.straight(1, 0).v)
|
|
|
|
def normal(self, t=0):
|
|
"""
|
|
2d Line perpendicular on plane xy
|
|
at position t in current segment
|
|
lie on the right side
|
|
p1
|
|
|--x
|
|
p0
|
|
"""
|
|
return Line(self.lerp(t), self.cross_z)
|
|
|
|
def sized_normal(self, t, size):
|
|
"""
|
|
2d Line perpendicular on plane xy
|
|
at position t in current segment
|
|
and of given length
|
|
lie on the right side when size > 0
|
|
p1
|
|
|--x
|
|
p0
|
|
"""
|
|
return Line(self.lerp(t), size * self.cross_z.normalized())
|
|
|
|
def lerp(self, t):
|
|
"""
|
|
3d interpolation
|
|
"""
|
|
return self.p + self.v * t
|
|
|
|
def intersect(self, line):
|
|
"""
|
|
2d intersection on plane xy
|
|
return
|
|
True if intersect
|
|
p: point of intersection
|
|
t: param t of intersection on current line
|
|
"""
|
|
c = line.cross_z
|
|
d = self.v.dot(c)
|
|
if d == 0:
|
|
return False, 0, 0
|
|
t = c.dot(line.p - self.p) / d
|
|
return True, self.lerp(t), t
|
|
|
|
def intersect_ext(self, line):
|
|
"""
|
|
same as intersect, but return param t on both lines
|
|
"""
|
|
c = line.cross_z
|
|
d = self.v.dot(c)
|
|
if d == 0:
|
|
return False, 0, 0, 0
|
|
dp = line.p - self.p
|
|
c2 = self.cross_z
|
|
u = c.dot(dp) / d
|
|
v = c2.dot(dp) / d
|
|
return u > 0 and v > 0 and u < 1 and v < 1, self.lerp(u), u, v
|
|
|
|
def point_sur_segment(self, pt):
|
|
""" _point_sur_segment
|
|
point: Vector 2d
|
|
t: param t de l'intersection sur le segment courant
|
|
d: distance laterale perpendiculaire positif a droite
|
|
"""
|
|
dp = pt - self.p
|
|
dl = self.length
|
|
if dl == 0:
|
|
return dp.length < 0.00001, 0, 0
|
|
d = (self.v.x * dp.y - self.v.y * dp.x) / dl
|
|
t = self.v.dot(dp) / (dl * dl)
|
|
return t > 0 and t < 1, d, t
|
|
|
|
def steps(self, len):
|
|
steps = max(1, round(self.length / len, 0))
|
|
return 1 / steps, int(steps)
|
|
|
|
def in_place_offset(self, offset):
|
|
"""
|
|
Offset current line
|
|
offset > 0 on the right part
|
|
"""
|
|
self.p += offset * self.cross_z.normalized()
|
|
|
|
def offset(self, offset):
|
|
"""
|
|
Return a new line
|
|
offset > 0 on the right part
|
|
"""
|
|
return Line(self.p + offset * self.cross_z.normalized(), self.v)
|
|
|
|
def tangeant(self, t, da, radius):
|
|
p = self.lerp(t)
|
|
if da < 0:
|
|
c = p + radius * self.cross_z.normalized()
|
|
else:
|
|
c = p - radius * self.cross_z.normalized()
|
|
return Arc(c, radius, self.angle_normal, da)
|
|
|
|
def straight(self, length, t=1):
|
|
return Line(self.lerp(t), self.v.normalized() * length)
|
|
|
|
def translate(self, dp):
|
|
self.p += dp
|
|
|
|
def rotate(self, a):
|
|
"""
|
|
Rotate segment ccw arroud p0
|
|
"""
|
|
ca = cos(a)
|
|
sa = sin(a)
|
|
self.v = Matrix([
|
|
[ca, -sa],
|
|
[sa, ca]
|
|
]) @ self.v
|
|
return self
|
|
|
|
def scale(self, length):
|
|
self.v = length * self.v.normalized()
|
|
return self
|
|
|
|
def tangeant_unit_vector(self, t):
|
|
return self.v.normalized()
|
|
|
|
def as_curve(self, context):
|
|
"""
|
|
Draw Line with open gl in screen space
|
|
aka: coords are in pixels
|
|
"""
|
|
curve = bpy.data.curves.new('LINE', type='CURVE')
|
|
curve.dimensions = '2D'
|
|
spline = curve.splines.new('POLY')
|
|
spline.use_endpoint_u = False
|
|
spline.use_cyclic_u = False
|
|
pts = self.pts
|
|
spline.points.add(len(pts) - 1)
|
|
for i, p in enumerate(pts):
|
|
x, y, z = p
|
|
spline.points[i].co = (x, y, 0, 1)
|
|
curve_obj = bpy.data.objects.new('LINE', curve)
|
|
context.scene.collection.objects.link(curve_obj)
|
|
curve_obj.select_set(state=True)
|
|
|
|
def make_offset(self, offset, last=None):
|
|
"""
|
|
Return offset between last and self.
|
|
Adjust last and self start to match
|
|
intersection point
|
|
"""
|
|
line = self.offset(offset)
|
|
if last is None:
|
|
return line
|
|
|
|
if hasattr(last, "r"):
|
|
res, d, t = line.point_sur_segment(last.c)
|
|
c = (last.r * last.r) - (d * d)
|
|
# print("t:%s" % t)
|
|
if c <= 0:
|
|
# no intersection !
|
|
p0 = line.lerp(t)
|
|
else:
|
|
# center is past start of line
|
|
if t > 0:
|
|
p0 = line.lerp(t) - line.v.normalized() * sqrt(c)
|
|
else:
|
|
p0 = line.lerp(t) + line.v.normalized() * sqrt(c)
|
|
# compute da of arc
|
|
u = last.p0 - last.c
|
|
v = p0 - last.c
|
|
da = self.signed_angle(u, v)
|
|
# da is ccw
|
|
if last.ccw:
|
|
# da is cw
|
|
if da < 0:
|
|
# so take inverse
|
|
da = 2 * pi + da
|
|
elif da > 0:
|
|
# da is ccw
|
|
da = 2 * pi - da
|
|
last.da = da
|
|
line.p0 = p0
|
|
else:
|
|
# intersect line / line
|
|
# 1 line -> 2 line
|
|
c = line.cross_z
|
|
d = last.v.dot(c)
|
|
if d == 0:
|
|
return line
|
|
v = line.p - last.p
|
|
t = c.dot(v) / d
|
|
c2 = last.cross_z
|
|
u = c2.dot(v) / d
|
|
# intersect past this segment end
|
|
# or before last segment start
|
|
# print("u:%s t:%s" % (u, t))
|
|
if u > 1 or t < 0:
|
|
return line
|
|
p = last.lerp(t)
|
|
line.p0 = p
|
|
last.p1 = p
|
|
|
|
return line
|
|
|
|
@property
|
|
def pts(self):
|
|
return [self.p0.to_3d(), self.p1.to_3d()]
|
|
|
|
|
|
class Circle(Projection):
|
|
def __init__(self, c, radius):
|
|
Projection.__init__(self)
|
|
self.r = radius
|
|
self.r2 = radius * radius
|
|
self.c = c
|
|
|
|
def intersect(self, line):
|
|
v = line.p - self.c
|
|
A = line.v.dot(line.v)
|
|
B = 2 * v.dot(line.v)
|
|
C = v.dot(v) - self.r2
|
|
d = B * B - 4 * A * C
|
|
if A <= 0.0000001 or d < 0:
|
|
# dosent intersect, find closest point of line
|
|
res, d, t = line.point_sur_segment(self.c)
|
|
return False, line.lerp(t), t
|
|
elif d == 0:
|
|
t = -B / 2 * A
|
|
return True, line.lerp(t), t
|
|
else:
|
|
AA = 2 * A
|
|
dsq = sqrt(d)
|
|
t0 = (-B + dsq) / AA
|
|
t1 = (-B - dsq) / AA
|
|
if abs(t0) < abs(t1):
|
|
return True, line.lerp(t0), t0
|
|
else:
|
|
return True, line.lerp(t1), t1
|
|
|
|
def translate(self, dp):
|
|
self.c += dp
|
|
|
|
|
|
class Arc(Circle):
|
|
"""
|
|
Represent a 2d Arc
|
|
TODO:
|
|
make it possible to define an arc by start point end point and center
|
|
"""
|
|
def __init__(self, c, radius, a0, da):
|
|
"""
|
|
a0 and da arguments are in radians
|
|
c Vector 2d center
|
|
radius float radius
|
|
a0 radians start angle
|
|
da radians delta angle from start to end
|
|
a0 = 0 on the right side
|
|
a0 = pi on the left side
|
|
da > 0 CCW contrary-clockwise
|
|
da < 0 CW clockwise
|
|
stored internally as radians
|
|
"""
|
|
Circle.__init__(self, Vector(c).to_2d(), radius)
|
|
self.line = None
|
|
self.a0 = a0
|
|
self.da = da
|
|
|
|
@property
|
|
def angle(self):
|
|
"""
|
|
angle of vector p0 p1
|
|
"""
|
|
v = self.p1 - self.p0
|
|
return atan2(v.y, v.x)
|
|
|
|
@property
|
|
def ccw(self):
|
|
return self.da > 0
|
|
|
|
def signed_angle(self, u, v):
|
|
"""
|
|
signed angle between two vectors
|
|
"""
|
|
return atan2(u.x * v.y - u.y * v.x, u.x * v.x + u.y * v.y)
|
|
|
|
def delta_angle(self, last):
|
|
"""
|
|
signed delta angle between end of line and start of this one
|
|
this value is object's a0 for segment = self
|
|
"""
|
|
if last is None:
|
|
return self.a0
|
|
return self.signed_angle(last.straight(1, 1).v, self.straight(1, 0).v)
|
|
|
|
def scale_rot_matrix(self, u, v):
|
|
"""
|
|
given vector u and v (from and to p0 p1)
|
|
apply scale factor to radius and
|
|
return a matrix to rotate and scale
|
|
the center around u origin so
|
|
arc fit v
|
|
"""
|
|
# signed angle old new vectors (rotation)
|
|
a = self.signed_angle(u, v)
|
|
# scale factor
|
|
scale = v.length / u.length
|
|
ca = scale * cos(a)
|
|
sa = scale * sin(a)
|
|
return scale, Matrix([
|
|
[ca, -sa],
|
|
[sa, ca]
|
|
])
|
|
|
|
@property
|
|
def p0(self):
|
|
"""
|
|
start point of arc
|
|
"""
|
|
return self.lerp(0)
|
|
|
|
@property
|
|
def p1(self):
|
|
"""
|
|
end point of arc
|
|
"""
|
|
return self.lerp(1)
|
|
|
|
@p0.setter
|
|
def p0(self, p0):
|
|
"""
|
|
rotate and scale arc so it intersect p0 p1
|
|
da is not affected
|
|
"""
|
|
u = self.p0 - self.p1
|
|
v = p0 - self.p1
|
|
scale, rM = self.scale_rot_matrix(u, v)
|
|
self.c = self.p1 + rM @ (self.c - self.p1)
|
|
self.r *= scale
|
|
self.r2 = self.r * self.r
|
|
dp = p0 - self.c
|
|
self.a0 = atan2(dp.y, dp.x)
|
|
|
|
@p1.setter
|
|
def p1(self, p1):
|
|
"""
|
|
rotate and scale arc so it intersect p0 p1
|
|
da is not affected
|
|
"""
|
|
p0 = self.p0
|
|
u = self.p1 - p0
|
|
v = p1 - p0
|
|
|
|
scale, rM = self.scale_rot_matrix(u, v)
|
|
self.c = p0 + rM @ (self.c - p0)
|
|
self.r *= scale
|
|
self.r2 = self.r * self.r
|
|
dp = p0 - self.c
|
|
self.a0 = atan2(dp.y, dp.x)
|
|
|
|
@property
|
|
def length(self):
|
|
"""
|
|
arc length
|
|
"""
|
|
return self.r * abs(self.da)
|
|
|
|
@property
|
|
def oposite(self):
|
|
a0 = self.a0 + self.da
|
|
if a0 > pi:
|
|
a0 -= 2 * pi
|
|
if a0 < -pi:
|
|
a0 += 2 * pi
|
|
return Arc(self.c, self.r, a0, -self.da)
|
|
|
|
def normal(self, t=0):
|
|
"""
|
|
Perpendicular line starting at t
|
|
always on the right side
|
|
"""
|
|
p = self.lerp(t)
|
|
if self.da < 0:
|
|
return Line(p, self.c - p)
|
|
else:
|
|
return Line(p, p - self.c)
|
|
|
|
def sized_normal(self, t, size):
|
|
"""
|
|
Perpendicular line starting at t and of a length size
|
|
on the right side when size > 0
|
|
"""
|
|
p = self.lerp(t)
|
|
if self.da < 0:
|
|
v = self.c - p
|
|
else:
|
|
v = p - self.c
|
|
return Line(p, size * v.normalized())
|
|
|
|
def lerp(self, t):
|
|
"""
|
|
Interpolate along segment
|
|
t parameter [0, 1] where 0 is start of arc and 1 is end
|
|
"""
|
|
a = self.a0 + t * self.da
|
|
return self.c + Vector((self.r * cos(a), self.r * sin(a)))
|
|
|
|
def steps(self, length):
|
|
"""
|
|
Compute step count given desired step length
|
|
"""
|
|
steps = max(1, round(self.length / length, 0))
|
|
return 1.0 / steps, int(steps)
|
|
|
|
def intersect_ext(self, line):
|
|
"""
|
|
same as intersect, but return param t on both lines
|
|
"""
|
|
res, p, v = self.intersect(line)
|
|
v0 = self.p0 - self.c
|
|
v1 = p - self.c
|
|
u = self.signed_angle(v0, v1) / self.da
|
|
return res and u > 0 and v > 0 and u < 1 and v < 1, p, u, v
|
|
|
|
# this is for wall
|
|
def steps_by_angle(self, step_angle):
|
|
steps = max(1, round(abs(self.da) / step_angle, 0))
|
|
return 1.0 / steps, int(steps)
|
|
|
|
def as_lines(self, steps):
|
|
"""
|
|
convert Arc to lines
|
|
"""
|
|
res = []
|
|
p0 = self.lerp(0)
|
|
for step in range(steps):
|
|
p1 = self.lerp((step + 1) / steps)
|
|
s = Line(p0=p0, p1=p1)
|
|
res.append(s)
|
|
p0 = p1
|
|
|
|
if self.line is not None:
|
|
p0 = self.line.lerp(0)
|
|
for step in range(steps):
|
|
p1 = self.line.lerp((step + 1) / steps)
|
|
res[step].line = Line(p0=p0, p1=p1)
|
|
p0 = p1
|
|
return res
|
|
|
|
def offset(self, offset):
|
|
"""
|
|
Offset circle
|
|
offset > 0 on the right part
|
|
"""
|
|
if self.da > 0:
|
|
radius = self.r + offset
|
|
else:
|
|
radius = self.r - offset
|
|
return Arc(self.c, radius, self.a0, self.da)
|
|
|
|
def tangeant(self, t, length):
|
|
"""
|
|
Tangent line so we are able to chain Circle and lines
|
|
Beware, counterpart on Line does return an Arc !
|
|
"""
|
|
a = self.a0 + t * self.da
|
|
ca = cos(a)
|
|
sa = sin(a)
|
|
p = self.c + Vector((self.r * ca, self.r * sa))
|
|
v = Vector((length * sa, -length * ca))
|
|
if self.da > 0:
|
|
v = -v
|
|
return Line(p, v)
|
|
|
|
def tangeant_unit_vector(self, t):
|
|
"""
|
|
Return Tangent vector of length 1
|
|
"""
|
|
a = self.a0 + t * self.da
|
|
ca = cos(a)
|
|
sa = sin(a)
|
|
v = Vector((sa, -ca))
|
|
if self.da > 0:
|
|
v = -v
|
|
return v
|
|
|
|
def straight(self, length, t=1):
|
|
"""
|
|
Return a tangent Line
|
|
Counterpart on Line also return a Line
|
|
"""
|
|
return self.tangeant(t, length)
|
|
|
|
def point_sur_segment(self, pt):
|
|
"""
|
|
Point pt lie on arc ?
|
|
return
|
|
True when pt lie on segment
|
|
t [0, 1] where it lie (normalized between start and end)
|
|
d distance from arc
|
|
"""
|
|
dp = pt - self.c
|
|
d = dp.length - self.r
|
|
a = atan2(dp.y, dp.x)
|
|
t = (a - self.a0) / self.da
|
|
return t > 0 and t < 1, d, t
|
|
|
|
def rotate(self, a):
|
|
"""
|
|
Rotate center so we rotate ccw around p0
|
|
"""
|
|
ca = cos(a)
|
|
sa = sin(a)
|
|
rM = Matrix([
|
|
[ca, -sa],
|
|
[sa, ca]
|
|
])
|
|
p0 = self.p0
|
|
self.c = p0 + rM @ (self.c - p0)
|
|
dp = p0 - self.c
|
|
self.a0 = atan2(dp.y, dp.x)
|
|
return self
|
|
|
|
# make offset for line / arc, arc / arc
|
|
def make_offset(self, offset, last=None):
|
|
|
|
line = self.offset(offset)
|
|
|
|
if last is None:
|
|
return line
|
|
|
|
if hasattr(last, "v"):
|
|
# intersect line / arc
|
|
# 1 line -> 2 arc
|
|
res, d, t = last.point_sur_segment(line.c)
|
|
c = line.r2 - (d * d)
|
|
if c <= 0:
|
|
# no intersection !
|
|
p0 = last.lerp(t)
|
|
else:
|
|
|
|
# center is past end of line
|
|
if t > 1:
|
|
# Arc take precedence
|
|
p0 = last.lerp(t) - last.v.normalized() * sqrt(c)
|
|
else:
|
|
# line take precedence
|
|
p0 = last.lerp(t) + last.v.normalized() * sqrt(c)
|
|
|
|
# compute a0 and da of arc
|
|
u = p0 - line.c
|
|
v = line.p1 - line.c
|
|
line.a0 = atan2(u.y, u.x)
|
|
da = self.signed_angle(u, v)
|
|
# da is ccw
|
|
if self.ccw:
|
|
# da is cw
|
|
if da < 0:
|
|
# so take inverse
|
|
da = 2 * pi + da
|
|
elif da > 0:
|
|
# da is ccw
|
|
da = 2 * pi - da
|
|
line.da = da
|
|
last.p1 = p0
|
|
else:
|
|
# intersect arc / arc x1 = self x0 = last
|
|
# rule to determine right side ->
|
|
# same side of d as p0 of self
|
|
dc = line.c - last.c
|
|
tmp = Line(last.c, dc)
|
|
res, d, t = tmp.point_sur_segment(self.p0)
|
|
r = line.r + last.r
|
|
dist = dc.length
|
|
if dist > r or \
|
|
dist < abs(last.r - self.r):
|
|
# no intersection
|
|
return line
|
|
if dist == r:
|
|
# 1 solution
|
|
p0 = dc * -last.r / r + self.c
|
|
else:
|
|
# 2 solutions
|
|
a = (last.r2 - line.r2 + dist * dist) / (2.0 * dist)
|
|
v2 = last.c + dc * a / dist
|
|
h = sqrt(last.r2 - a * a)
|
|
r = Vector((-dc.y, dc.x)) * (h / dist)
|
|
p0 = v2 + r
|
|
res, d1, t = tmp.point_sur_segment(p0)
|
|
# take other point if we are not on the same side
|
|
if d1 > 0:
|
|
if d < 0:
|
|
p0 = v2 - r
|
|
elif d > 0:
|
|
p0 = v2 - r
|
|
|
|
# compute da of last
|
|
u = last.p0 - last.c
|
|
v = p0 - last.c
|
|
last.da = self.signed_angle(u, v)
|
|
|
|
# compute a0 and da of current
|
|
u, v = v, line.p1 - line.c
|
|
line.a0 = atan2(u.y, u.x)
|
|
line.da = self.signed_angle(u, v)
|
|
return line
|
|
|
|
# DEBUG
|
|
@property
|
|
def pts(self):
|
|
n_pts = max(1, int(round(abs(self.da) / pi * 30, 0)))
|
|
t_step = 1 / n_pts
|
|
return [self.lerp(i * t_step).to_3d() for i in range(n_pts + 1)]
|
|
|
|
def as_curve(self, context):
|
|
"""
|
|
Draw 2d arc with open gl in screen space
|
|
aka: coords are in pixels
|
|
"""
|
|
curve = bpy.data.curves.new('ARC', type='CURVE')
|
|
curve.dimensions = '2D'
|
|
spline = curve.splines.new('POLY')
|
|
spline.use_endpoint_u = False
|
|
spline.use_cyclic_u = False
|
|
pts = self.pts
|
|
spline.points.add(len(pts) - 1)
|
|
for i, p in enumerate(pts):
|
|
x, y = p
|
|
spline.points[i].co = (x, y, 0, 1)
|
|
curve_obj = bpy.data.objects.new('ARC', curve)
|
|
context.scene.collection.objects.link(curve_obj)
|
|
curve_obj.select_set(state=True)
|
|
|
|
|
|
class Line3d(Line):
|
|
"""
|
|
3d Line
|
|
mostly a gl enabled for future use in manipulators
|
|
coords are in world space
|
|
"""
|
|
def __init__(self, p=None, v=None, p0=None, p1=None, z_axis=None):
|
|
"""
|
|
Init by either
|
|
p: Vector or tuple origin
|
|
v: Vector or tuple size and direction
|
|
or
|
|
p0: Vector or tuple 1 point location
|
|
p1: Vector or tuple 2 point location
|
|
Will convert any into Vector 3d
|
|
both optionnals
|
|
"""
|
|
if p is not None and v is not None:
|
|
self.p = Vector(p).to_3d()
|
|
self.v = Vector(v).to_3d()
|
|
elif p0 is not None and p1 is not None:
|
|
self.p = Vector(p0).to_3d()
|
|
self.v = Vector(p1).to_3d() - self.p
|
|
else:
|
|
self.p = Vector((0, 0, 0))
|
|
self.v = Vector((0, 0, 0))
|
|
if z_axis is not None:
|
|
self.z_axis = z_axis
|
|
else:
|
|
self.z_axis = Vector((0, 0, 1))
|
|
|
|
@property
|
|
def p0(self):
|
|
return self.p
|
|
|
|
@property
|
|
def p1(self):
|
|
return self.p + self.v
|
|
|
|
@p0.setter
|
|
def p0(self, p0):
|
|
"""
|
|
Note: setting p0
|
|
move p0 only
|
|
"""
|
|
p1 = self.p1
|
|
self.p = Vector(p0).to_3d()
|
|
self.v = p1 - p0
|
|
|
|
@p1.setter
|
|
def p1(self, p1):
|
|
"""
|
|
Note: setting p1
|
|
move p1 only
|
|
"""
|
|
self.v = Vector(p1).to_3d() - self.p
|
|
|
|
@property
|
|
def cross_z(self):
|
|
"""
|
|
3d Vector perpendicular on plane xy
|
|
lie on the right side
|
|
p1
|
|
|--x
|
|
p0
|
|
"""
|
|
return self.v.cross(Vector((0, 0, 1)))
|
|
|
|
@property
|
|
def cross(self):
|
|
"""
|
|
3d Vector perpendicular on plane defined by z_axis
|
|
lie on the right side
|
|
p1
|
|
|--x
|
|
p0
|
|
"""
|
|
return self.v.cross(self.z_axis)
|
|
|
|
def normal(self, t=0):
|
|
"""
|
|
3d Vector perpendicular on plane defined by z_axis
|
|
lie on the right side
|
|
p1
|
|
|--x
|
|
p0
|
|
"""
|
|
n = Line3d()
|
|
n.p = self.lerp(t)
|
|
n.v = self.cross
|
|
return n
|
|
|
|
def sized_normal(self, t, size):
|
|
"""
|
|
3d Line perpendicular on plane defined by z_axis and of given size
|
|
positioned at t in current line
|
|
lie on the right side
|
|
p1
|
|
|--x
|
|
p0
|
|
"""
|
|
p = self.lerp(t)
|
|
v = size * self.cross.normalized()
|
|
return Line3d(p, v, z_axis=self.z_axis)
|
|
|
|
def offset(self, offset):
|
|
"""
|
|
offset > 0 on the right part
|
|
"""
|
|
return Line3d(self.p + offset * self.cross.normalized(), self.v)
|
|
|
|
# unless override, 2d methods should raise NotImplementedError
|
|
def intersect(self, line):
|
|
raise NotImplementedError
|
|
|
|
def point_sur_segment(self, pt):
|
|
raise NotImplementedError
|
|
|
|
def tangeant(self, t, da, radius):
|
|
raise NotImplementedError
|