Sun position: remove unused prop in HDRI mode
[blender-addons.git] / archipack / archipack_2d.py
blobe286e730c1608c8884fa9685f3cb0ae3d9388df8
1 # -*- coding:utf-8 -*-
3 # ##### BEGIN GPL LICENSE BLOCK #####
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software Foundation,
17 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
19 # ##### END GPL LICENSE BLOCK #####
21 # <pep8 compliant>
23 # ----------------------------------------------------------
24 # Author: Stephen Leger (s-leger)
26 # ----------------------------------------------------------
27 from mathutils import Vector, Matrix
28 from math import sin, cos, pi, atan2, sqrt, acos
29 import bpy
30 # allow to draw parts with gl for debug puropses
31 from .archipack_gl import GlBaseLine
34 class Projection(GlBaseLine):
36 def __init__(self):
37 GlBaseLine.__init__(self)
39 def proj_xy(self, t, next=None):
40 """
41 length of projection of sections at crossing line / circle intersections
42 deformation unit vector for profil in xy axis
43 so f(x_profile) = position of point in xy plane
44 """
45 if next is None:
46 return self.normal(t).v.normalized(), 1
47 v0 = self.normal(1).v.normalized()
48 v1 = next.normal(0).v.normalized()
49 direction = v0 + v1
50 adj = (v0 * self.length) * (v1 * next.length)
51 hyp = (self.length * next.length)
52 c = min(1, max(-1, adj / hyp))
53 size = 1 / cos(0.5 * acos(c))
54 return direction.normalized(), min(3, size)
56 def proj_z(self, t, dz0, next=None, dz1=0):
57 """
58 length of projection along crossing line / circle
59 deformation unit vector for profil in z axis at line / line intersection
60 so f(y) = position of point in yz plane
61 """
62 return Vector((0, 1)), 1
63 """
64 NOTE (to myself):
65 In theory this is how it has to be done so sections follow path,
66 but in real world results are better when sections are z-up.
67 So return a dumb 1 so f(y) = y
68 """
69 if next is None:
70 dz = dz0 / self.length
71 else:
72 dz = (dz1 + dz0) / (self.length + next.length)
73 return Vector((0, 1)), sqrt(1 + dz * dz)
74 # 1 / sqrt(1 + (dz0 / self.length) * (dz0 / self.length))
75 if next is None:
76 return Vector((-dz0, self.length)).normalized(), 1
77 v0 = Vector((self.length, dz0))
78 v1 = Vector((next.length, dz1))
79 direction = Vector((-dz0, self.length)).normalized() + Vector((-dz1, next.length)).normalized()
80 adj = v0 * v1
81 hyp = (v0.length * v1.length)
82 c = min(1, max(-1, adj / hyp))
83 size = -cos(pi - 0.5 * acos(c))
84 return direction.normalized(), size
87 class Line(Projection):
88 """
89 2d Line
90 Internally stored as p: origin and v:size and direction
91 moving p will move both ends of line
92 moving p0 or p1 move only one end of line
95 | v
96 p0 == p
97 """
98 def __init__(self, p=None, v=None, p0=None, p1=None):
99 """
100 Init by either
101 p: Vector or tuple origin
102 v: Vector or tuple size and direction
104 p0: Vector or tuple 1 point location
105 p1: Vector or tuple 2 point location
106 Will convert any into Vector 2d
107 both optionnals
109 Projection.__init__(self)
110 if p is not None and v is not None:
111 self.p = Vector(p).to_2d()
112 self.v = Vector(v).to_2d()
113 elif p0 is not None and p1 is not None:
114 self.p = Vector(p0).to_2d()
115 self.v = Vector(p1).to_2d() - self.p
116 else:
117 self.p = Vector((0, 0))
118 self.v = Vector((0, 0))
119 self.line = None
121 @property
122 def copy(self):
123 return Line(self.p.copy(), self.v.copy())
125 @property
126 def p0(self):
127 return self.p
129 @property
130 def p1(self):
131 return self.p + self.v
133 @p0.setter
134 def p0(self, p0):
136 Note: setting p0
137 move p0 only
139 p1 = self.p1
140 self.p = Vector(p0).to_2d()
141 self.v = p1 - p0
143 @p1.setter
144 def p1(self, p1):
146 Note: setting p1
147 move p1 only
149 self.v = Vector(p1).to_2d() - self.p
151 @property
152 def length(self):
154 3d length
156 return self.v.length
158 @property
159 def angle(self):
161 2d angle on xy plane
163 return atan2(self.v.y, self.v.x)
165 @property
166 def a0(self):
167 return self.angle
169 @property
170 def angle_normal(self):
172 2d angle of perpendicular
173 lie on the right side
175 |--x
178 return atan2(-self.v.x, self.v.y)
180 @property
181 def reversed(self):
182 return Line(self.p, -self.v)
184 @property
185 def oposite(self):
186 return Line(self.p + self.v, -self.v)
188 @property
189 def cross_z(self):
191 2d Vector perpendicular on plane xy
192 lie on the right side
194 |--x
197 return Vector((self.v.y, -self.v.x))
199 @property
200 def cross(self):
201 return Vector((self.v.y, -self.v.x))
203 def signed_angle(self, u, v):
205 signed angle between two vectors range [-pi, pi]
207 return atan2(u.x * v.y - u.y * v.x, u.x * v.x + u.y * v.y)
209 def delta_angle(self, last):
211 signed delta angle between end of line and start of this one
212 this value is object's a0 for segment = self
214 if last is None:
215 return self.angle
216 return self.signed_angle(last.straight(1, 1).v, self.straight(1, 0).v)
218 def normal(self, t=0):
220 2d Line perpendicular on plane xy
221 at position t in current segment
222 lie on the right side
224 |--x
227 return Line(self.lerp(t), self.cross_z)
229 def sized_normal(self, t, size):
231 2d Line perpendicular on plane xy
232 at position t in current segment
233 and of given length
234 lie on the right side when size > 0
236 |--x
239 return Line(self.lerp(t), size * self.cross_z.normalized())
241 def lerp(self, t):
243 3d interpolation
245 return self.p + self.v * t
247 def intersect(self, line):
249 2d intersection on plane xy
250 return
251 True if intersect
252 p: point of intersection
253 t: param t of intersection on current line
255 c = line.cross_z
256 d = self.v.dot(c)
257 if d == 0:
258 return False, 0, 0
259 t = c.dot(line.p - self.p) / d
260 return True, self.lerp(t), t
262 def intersect_ext(self, line):
264 same as intersect, but return param t on both lines
266 c = line.cross_z
267 d = self.v.dot(c)
268 if d == 0:
269 return False, 0, 0, 0
270 dp = line.p - self.p
271 c2 = self.cross_z
272 u = c.dot(dp) / d
273 v = c2.dot(dp) / d
274 return u > 0 and v > 0 and u < 1 and v < 1, self.lerp(u), u, v
276 def point_sur_segment(self, pt):
277 """ _point_sur_segment
278 point: Vector 2d
279 t: param t de l'intersection sur le segment courant
280 d: distance laterale perpendiculaire positif a droite
282 dp = pt - self.p
283 dl = self.length
284 if dl == 0:
285 return dp.length < 0.00001, 0, 0
286 d = (self.v.x * dp.y - self.v.y * dp.x) / dl
287 t = self.v.dot(dp) / (dl * dl)
288 return t > 0 and t < 1, d, t
290 def steps(self, len):
291 steps = max(1, round(self.length / len, 0))
292 return 1 / steps, int(steps)
294 def in_place_offset(self, offset):
296 Offset current line
297 offset > 0 on the right part
299 self.p += offset * self.cross_z.normalized()
301 def offset(self, offset):
303 Return a new line
304 offset > 0 on the right part
306 return Line(self.p + offset * self.cross_z.normalized(), self.v)
308 def tangeant(self, t, da, radius):
309 p = self.lerp(t)
310 if da < 0:
311 c = p + radius * self.cross_z.normalized()
312 else:
313 c = p - radius * self.cross_z.normalized()
314 return Arc(c, radius, self.angle_normal, da)
316 def straight(self, length, t=1):
317 return Line(self.lerp(t), self.v.normalized() * length)
319 def translate(self, dp):
320 self.p += dp
322 def rotate(self, a):
324 Rotate segment ccw arroud p0
326 ca = cos(a)
327 sa = sin(a)
328 self.v = Matrix([
329 [ca, -sa],
330 [sa, ca]
331 ]) @ self.v
332 return self
334 def scale(self, length):
335 self.v = length * self.v.normalized()
336 return self
338 def tangeant_unit_vector(self, t):
339 return self.v.normalized()
341 def as_curve(self, context):
343 Draw Line with open gl in screen space
344 aka: coords are in pixels
346 curve = bpy.data.curves.new('LINE', type='CURVE')
347 curve.dimensions = '2D'
348 spline = curve.splines.new('POLY')
349 spline.use_endpoint_u = False
350 spline.use_cyclic_u = False
351 pts = self.pts
352 spline.points.add(len(pts) - 1)
353 for i, p in enumerate(pts):
354 x, y, z = p
355 spline.points[i].co = (x, y, 0, 1)
356 curve_obj = bpy.data.objects.new('LINE', curve)
357 context.scene.collection.objects.link(curve_obj)
358 curve_obj.select_set(state=True)
360 def make_offset(self, offset, last=None):
362 Return offset between last and self.
363 Adjust last and self start to match
364 intersection point
366 line = self.offset(offset)
367 if last is None:
368 return line
370 if hasattr(last, "r"):
371 res, d, t = line.point_sur_segment(last.c)
372 c = (last.r * last.r) - (d * d)
373 # print("t:%s" % t)
374 if c <= 0:
375 # no intersection !
376 p0 = line.lerp(t)
377 else:
378 # center is past start of line
379 if t > 0:
380 p0 = line.lerp(t) - line.v.normalized() * sqrt(c)
381 else:
382 p0 = line.lerp(t) + line.v.normalized() * sqrt(c)
383 # compute da of arc
384 u = last.p0 - last.c
385 v = p0 - last.c
386 da = self.signed_angle(u, v)
387 # da is ccw
388 if last.ccw:
389 # da is cw
390 if da < 0:
391 # so take inverse
392 da = 2 * pi + da
393 elif da > 0:
394 # da is ccw
395 da = 2 * pi - da
396 last.da = da
397 line.p0 = p0
398 else:
399 # intersect line / line
400 # 1 line -> 2 line
401 c = line.cross_z
402 d = last.v.dot(c)
403 if d == 0:
404 return line
405 v = line.p - last.p
406 t = c.dot(v) / d
407 c2 = last.cross_z
408 u = c2.dot(v) / d
409 # intersect past this segment end
410 # or before last segment start
411 # print("u:%s t:%s" % (u, t))
412 if u > 1 or t < 0:
413 return line
414 p = last.lerp(t)
415 line.p0 = p
416 last.p1 = p
418 return line
420 @property
421 def pts(self):
422 return [self.p0.to_3d(), self.p1.to_3d()]
425 class Circle(Projection):
426 def __init__(self, c, radius):
427 Projection.__init__(self)
428 self.r = radius
429 self.r2 = radius * radius
430 self.c = c
432 def intersect(self, line):
433 v = line.p - self.c
434 A = line.v.dot(line.v)
435 B = 2 * v.dot(line.v)
436 C = v.dot(v) - self.r2
437 d = B * B - 4 * A * C
438 if A <= 0.0000001 or d < 0:
439 # dosent intersect, find closest point of line
440 res, d, t = line.point_sur_segment(self.c)
441 return False, line.lerp(t), t
442 elif d == 0:
443 t = -B / 2 * A
444 return True, line.lerp(t), t
445 else:
446 AA = 2 * A
447 dsq = sqrt(d)
448 t0 = (-B + dsq) / AA
449 t1 = (-B - dsq) / AA
450 if abs(t0) < abs(t1):
451 return True, line.lerp(t0), t0
452 else:
453 return True, line.lerp(t1), t1
455 def translate(self, dp):
456 self.c += dp
459 class Arc(Circle):
461 Represent a 2d Arc
462 TODO:
463 make it possible to define an arc by start point end point and center
465 def __init__(self, c, radius, a0, da):
467 a0 and da arguments are in radians
468 c Vector 2d center
469 radius float radius
470 a0 radians start angle
471 da radians delta angle from start to end
472 a0 = 0 on the right side
473 a0 = pi on the left side
474 da > 0 CCW contrary-clockwise
475 da < 0 CW clockwise
476 stored internally as radians
478 Circle.__init__(self, Vector(c).to_2d(), radius)
479 self.line = None
480 self.a0 = a0
481 self.da = da
483 @property
484 def angle(self):
486 angle of vector p0 p1
488 v = self.p1 - self.p0
489 return atan2(v.y, v.x)
491 @property
492 def ccw(self):
493 return self.da > 0
495 def signed_angle(self, u, v):
497 signed angle between two vectors
499 return atan2(u.x * v.y - u.y * v.x, u.x * v.x + u.y * v.y)
501 def delta_angle(self, last):
503 signed delta angle between end of line and start of this one
504 this value is object's a0 for segment = self
506 if last is None:
507 return self.a0
508 return self.signed_angle(last.straight(1, 1).v, self.straight(1, 0).v)
510 def scale_rot_matrix(self, u, v):
512 given vector u and v (from and to p0 p1)
513 apply scale factor to radius and
514 return a matrix to rotate and scale
515 the center around u origin so
516 arc fit v
518 # signed angle old new vectors (rotation)
519 a = self.signed_angle(u, v)
520 # scale factor
521 scale = v.length / u.length
522 ca = scale * cos(a)
523 sa = scale * sin(a)
524 return scale, Matrix([
525 [ca, -sa],
526 [sa, ca]
529 @property
530 def p0(self):
532 start point of arc
534 return self.lerp(0)
536 @property
537 def p1(self):
539 end point of arc
541 return self.lerp(1)
543 @p0.setter
544 def p0(self, p0):
546 rotate and scale arc so it intersect p0 p1
547 da is not affected
549 u = self.p0 - self.p1
550 v = p0 - self.p1
551 scale, rM = self.scale_rot_matrix(u, v)
552 self.c = self.p1 + rM @ (self.c - self.p1)
553 self.r *= scale
554 self.r2 = self.r * self.r
555 dp = p0 - self.c
556 self.a0 = atan2(dp.y, dp.x)
558 @p1.setter
559 def p1(self, p1):
561 rotate and scale arc so it intersect p0 p1
562 da is not affected
564 p0 = self.p0
565 u = self.p1 - p0
566 v = p1 - p0
568 scale, rM = self.scale_rot_matrix(u, v)
569 self.c = p0 + rM @ (self.c - p0)
570 self.r *= scale
571 self.r2 = self.r * self.r
572 dp = p0 - self.c
573 self.a0 = atan2(dp.y, dp.x)
575 @property
576 def length(self):
578 arc length
580 return self.r * abs(self.da)
582 @property
583 def oposite(self):
584 a0 = self.a0 + self.da
585 if a0 > pi:
586 a0 -= 2 * pi
587 if a0 < -pi:
588 a0 += 2 * pi
589 return Arc(self.c, self.r, a0, -self.da)
591 def normal(self, t=0):
593 Perpendicular line starting at t
594 always on the right side
596 p = self.lerp(t)
597 if self.da < 0:
598 return Line(p, self.c - p)
599 else:
600 return Line(p, p - self.c)
602 def sized_normal(self, t, size):
604 Perpendicular line starting at t and of a length size
605 on the right side when size > 0
607 p = self.lerp(t)
608 if self.da < 0:
609 v = self.c - p
610 else:
611 v = p - self.c
612 return Line(p, size * v.normalized())
614 def lerp(self, t):
616 Interpolate along segment
617 t parameter [0, 1] where 0 is start of arc and 1 is end
619 a = self.a0 + t * self.da
620 return self.c + Vector((self.r * cos(a), self.r * sin(a)))
622 def steps(self, length):
624 Compute step count given desired step length
626 steps = max(1, round(self.length / length, 0))
627 return 1.0 / steps, int(steps)
629 def intersect_ext(self, line):
631 same as intersect, but return param t on both lines
633 res, p, v = self.intersect(line)
634 v0 = self.p0 - self.c
635 v1 = p - self.c
636 u = self.signed_angle(v0, v1) / self.da
637 return res and u > 0 and v > 0 and u < 1 and v < 1, p, u, v
639 # this is for wall
640 def steps_by_angle(self, step_angle):
641 steps = max(1, round(abs(self.da) / step_angle, 0))
642 return 1.0 / steps, int(steps)
644 def as_lines(self, steps):
646 convert Arc to lines
648 res = []
649 p0 = self.lerp(0)
650 for step in range(steps):
651 p1 = self.lerp((step + 1) / steps)
652 s = Line(p0=p0, p1=p1)
653 res.append(s)
654 p0 = p1
656 if self.line is not None:
657 p0 = self.line.lerp(0)
658 for step in range(steps):
659 p1 = self.line.lerp((step + 1) / steps)
660 res[step].line = Line(p0=p0, p1=p1)
661 p0 = p1
662 return res
664 def offset(self, offset):
666 Offset circle
667 offset > 0 on the right part
669 if self.da > 0:
670 radius = self.r + offset
671 else:
672 radius = self.r - offset
673 return Arc(self.c, radius, self.a0, self.da)
675 def tangeant(self, t, length):
677 Tangent line so we are able to chain Circle and lines
678 Beware, counterpart on Line does return an Arc !
680 a = self.a0 + t * self.da
681 ca = cos(a)
682 sa = sin(a)
683 p = self.c + Vector((self.r * ca, self.r * sa))
684 v = Vector((length * sa, -length * ca))
685 if self.da > 0:
686 v = -v
687 return Line(p, v)
689 def tangeant_unit_vector(self, t):
691 Return Tangent vector of length 1
693 a = self.a0 + t * self.da
694 ca = cos(a)
695 sa = sin(a)
696 v = Vector((sa, -ca))
697 if self.da > 0:
698 v = -v
699 return v
701 def straight(self, length, t=1):
703 Return a tangent Line
704 Counterpart on Line also return a Line
706 return self.tangeant(t, length)
708 def point_sur_segment(self, pt):
710 Point pt lie on arc ?
711 return
712 True when pt lie on segment
713 t [0, 1] where it lie (normalized between start and end)
714 d distance from arc
716 dp = pt - self.c
717 d = dp.length - self.r
718 a = atan2(dp.y, dp.x)
719 t = (a - self.a0) / self.da
720 return t > 0 and t < 1, d, t
722 def rotate(self, a):
724 Rotate center so we rotate ccw around p0
726 ca = cos(a)
727 sa = sin(a)
728 rM = Matrix([
729 [ca, -sa],
730 [sa, ca]
732 p0 = self.p0
733 self.c = p0 + rM @ (self.c - p0)
734 dp = p0 - self.c
735 self.a0 = atan2(dp.y, dp.x)
736 return self
738 # make offset for line / arc, arc / arc
739 def make_offset(self, offset, last=None):
741 line = self.offset(offset)
743 if last is None:
744 return line
746 if hasattr(last, "v"):
747 # intersect line / arc
748 # 1 line -> 2 arc
749 res, d, t = last.point_sur_segment(line.c)
750 c = line.r2 - (d * d)
751 if c <= 0:
752 # no intersection !
753 p0 = last.lerp(t)
754 else:
756 # center is past end of line
757 if t > 1:
758 # Arc take precedence
759 p0 = last.lerp(t) - last.v.normalized() * sqrt(c)
760 else:
761 # line take precedence
762 p0 = last.lerp(t) + last.v.normalized() * sqrt(c)
764 # compute a0 and da of arc
765 u = p0 - line.c
766 v = line.p1 - line.c
767 line.a0 = atan2(u.y, u.x)
768 da = self.signed_angle(u, v)
769 # da is ccw
770 if self.ccw:
771 # da is cw
772 if da < 0:
773 # so take inverse
774 da = 2 * pi + da
775 elif da > 0:
776 # da is ccw
777 da = 2 * pi - da
778 line.da = da
779 last.p1 = p0
780 else:
781 # intersect arc / arc x1 = self x0 = last
782 # rule to determine right side ->
783 # same side of d as p0 of self
784 dc = line.c - last.c
785 tmp = Line(last.c, dc)
786 res, d, t = tmp.point_sur_segment(self.p0)
787 r = line.r + last.r
788 dist = dc.length
789 if dist > r or \
790 dist < abs(last.r - self.r):
791 # no intersection
792 return line
793 if dist == r:
794 # 1 solution
795 p0 = dc * -last.r / r + self.c
796 else:
797 # 2 solutions
798 a = (last.r2 - line.r2 + dist * dist) / (2.0 * dist)
799 v2 = last.c + dc * a / dist
800 h = sqrt(last.r2 - a * a)
801 r = Vector((-dc.y, dc.x)) * (h / dist)
802 p0 = v2 + r
803 res, d1, t = tmp.point_sur_segment(p0)
804 # take other point if we are not on the same side
805 if d1 > 0:
806 if d < 0:
807 p0 = v2 - r
808 elif d > 0:
809 p0 = v2 - r
811 # compute da of last
812 u = last.p0 - last.c
813 v = p0 - last.c
814 last.da = self.signed_angle(u, v)
816 # compute a0 and da of current
817 u, v = v, line.p1 - line.c
818 line.a0 = atan2(u.y, u.x)
819 line.da = self.signed_angle(u, v)
820 return line
822 # DEBUG
823 @property
824 def pts(self):
825 n_pts = max(1, int(round(abs(self.da) / pi * 30, 0)))
826 t_step = 1 / n_pts
827 return [self.lerp(i * t_step).to_3d() for i in range(n_pts + 1)]
829 def as_curve(self, context):
831 Draw 2d arc with open gl in screen space
832 aka: coords are in pixels
834 curve = bpy.data.curves.new('ARC', type='CURVE')
835 curve.dimensions = '2D'
836 spline = curve.splines.new('POLY')
837 spline.use_endpoint_u = False
838 spline.use_cyclic_u = False
839 pts = self.pts
840 spline.points.add(len(pts) - 1)
841 for i, p in enumerate(pts):
842 x, y = p
843 spline.points[i].co = (x, y, 0, 1)
844 curve_obj = bpy.data.objects.new('ARC', curve)
845 context.scene.collection.objects.link(curve_obj)
846 curve_obj.select_set(state=True)
849 class Line3d(Line):
851 3d Line
852 mostly a gl enabled for future use in manipulators
853 coords are in world space
855 def __init__(self, p=None, v=None, p0=None, p1=None, z_axis=None):
857 Init by either
858 p: Vector or tuple origin
859 v: Vector or tuple size and direction
861 p0: Vector or tuple 1 point location
862 p1: Vector or tuple 2 point location
863 Will convert any into Vector 3d
864 both optionnals
866 if p is not None and v is not None:
867 self.p = Vector(p).to_3d()
868 self.v = Vector(v).to_3d()
869 elif p0 is not None and p1 is not None:
870 self.p = Vector(p0).to_3d()
871 self.v = Vector(p1).to_3d() - self.p
872 else:
873 self.p = Vector((0, 0, 0))
874 self.v = Vector((0, 0, 0))
875 if z_axis is not None:
876 self.z_axis = z_axis
877 else:
878 self.z_axis = Vector((0, 0, 1))
880 @property
881 def p0(self):
882 return self.p
884 @property
885 def p1(self):
886 return self.p + self.v
888 @p0.setter
889 def p0(self, p0):
891 Note: setting p0
892 move p0 only
894 p1 = self.p1
895 self.p = Vector(p0).to_3d()
896 self.v = p1 - p0
898 @p1.setter
899 def p1(self, p1):
901 Note: setting p1
902 move p1 only
904 self.v = Vector(p1).to_3d() - self.p
906 @property
907 def cross_z(self):
909 3d Vector perpendicular on plane xy
910 lie on the right side
912 |--x
915 return self.v.cross(Vector((0, 0, 1)))
917 @property
918 def cross(self):
920 3d Vector perpendicular on plane defined by z_axis
921 lie on the right side
923 |--x
926 return self.v.cross(self.z_axis)
928 def normal(self, t=0):
930 3d Vector perpendicular on plane defined by z_axis
931 lie on the right side
933 |--x
936 n = Line3d()
937 n.p = self.lerp(t)
938 n.v = self.cross
939 return n
941 def sized_normal(self, t, size):
943 3d Line perpendicular on plane defined by z_axis and of given size
944 positioned at t in current line
945 lie on the right side
947 |--x
950 p = self.lerp(t)
951 v = size * self.cross.normalized()
952 return Line3d(p, v, z_axis=self.z_axis)
954 def offset(self, offset):
956 offset > 0 on the right part
958 return Line3d(self.p + offset * self.cross.normalized(), self.v)
960 # unless override, 2d methods should raise NotImplementedError
961 def intersect(self, line):
962 raise NotImplementedError
964 def point_sur_segment(self, pt):
965 raise NotImplementedError
967 def tangeant(self, t, da, radius):
968 raise NotImplementedError