Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStephen Leger <stephen@3dservices.ch>2017-07-22 14:25:28 +0300
committerStephen Leger <stephen@3dservices.ch>2017-07-22 14:26:04 +0300
commitc1ab9b4b9c6c0226f8d7789b92efda9b0f33cfd1 (patch)
tree37d5a97c758fa9af48d1dfb5428edd72072d882a /archipack/archipack_2d.py
parent5638a8783502138500912061dde0e8ee476d7fca (diff)
archipack: T52120 release to official
Diffstat (limited to 'archipack/archipack_2d.py')
-rw-r--r--archipack/archipack_2d.py893
1 files changed, 893 insertions, 0 deletions
diff --git a/archipack/archipack_2d.py b/archipack/archipack_2d.py
new file mode 100644
index 00000000..912e3cb8
--- /dev/null
+++ b/archipack/archipack_2d.py
@@ -0,0 +1,893 @@
+# -*- 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))
+
+ @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 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 * c
+ if d == 0:
+ return False, 0, 0
+ t = (c * (line.p - self.p)) / d
+ return True, self.lerp(t), t
+
+ 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
+ d = (self.v.x * dp.y - self.v.y * dp.x) / dl
+ t = (self.v * 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
+ """
+ raise NotImplementedError
+
+ 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 * c
+ if d == 0:
+ return line
+ v = line.p - last.p
+ t = (c * v) / d
+ c2 = last.cross_z
+ u = (c2 * 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 * line.v
+ B = 2 * v * line.v
+ C = v * 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:
+ Add some sugar here
+ like being able to set p0 and p1 of line
+ 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.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)
+
+ 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)
+
+ # 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 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):
+ """
+ Tangeant 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 Tangeant 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 tangeant 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 arround 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.objects.link(curve_obj)
+ curve_obj.select = 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
+ positionned 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