diff options
Diffstat (limited to 'archipack/archipack_2d.py')
-rw-r--r-- | archipack/archipack_2d.py | 968 |
1 files changed, 0 insertions, 968 deletions
diff --git a/archipack/archipack_2d.py b/archipack/archipack_2d.py deleted file mode 100644 index e286e730..00000000 --- a/archipack/archipack_2d.py +++ /dev/null @@ -1,968 +0,0 @@ -# -*- 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 |