1# -*- coding:utf-8 -*-
2
3# ##### BEGIN GPL LICENSE BLOCK #####
4#
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.
9#
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.
14#
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.
18#
19# ##### END GPL LICENSE BLOCK #####
20
21# <pep8 compliant>
22
23# ----------------------------------------------------------
24# Author: Stephen Leger (s-leger)
25#
26# ----------------------------------------------------------
27from mathutils import Vector, Matrix
28from math import sin, cos, pi, atan2, sqrt, acos
29import bpy
30# allow to draw parts with gl for debug puropses
31from .archipack_gl import GlBaseLine
32
33
34class Projection(GlBaseLine):
35
36    def __init__(self):
37        GlBaseLine.__init__(self)
38
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)
55
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
85
86
87class 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
93            p1
94            ^
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
103            or
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
108        """
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
120
121    @property
122    def copy(self):
123        return Line(self.p.copy(), self.v.copy())
124
125    @property
126    def p0(self):
127        return self.p
128
129    @property
130    def p1(self):
131        return self.p + self.v
132
133    @p0.setter
134    def p0(self, p0):
135        """
136            Note: setting p0
137            move p0 only
138        """
139        p1 = self.p1
140        self.p = Vector(p0).to_2d()
141        self.v = p1 - p0
142
143    @p1.setter
144    def p1(self, p1):
145        """
146            Note: setting p1
147            move p1 only
148        """
149        self.v = Vector(p1).to_2d() - self.p
150
151    @property
152    def length(self):
153        """
154            3d length
155        """
156        return self.v.length
157
158    @property
159    def angle(self):
160        """
161            2d angle on xy plane
162        """
163        return atan2(self.v.y, self.v.x)
164
165    @property
166    def a0(self):
167        return self.angle
168
169    @property
170    def angle_normal(self):
171        """
172            2d angle of perpendicular
173            lie on the right side
174            p1
175            |--x
176            p0
177        """
178        return atan2(-self.v.x, self.v.y)
179
180    @property
181    def reversed(self):
182        return Line(self.p, -self.v)
183
184    @property
185    def oposite(self):
186        return Line(self.p + self.v, -self.v)
187
188    @property
189    def cross_z(self):
190        """
191            2d Vector perpendicular on plane xy
192            lie on the right side
193            p1
194            |--x
195            p0
196        """
197        return Vector((self.v.y, -self.v.x))
198
199    @property
200    def cross(self):
201        return Vector((self.v.y, -self.v.x))
202
203    def signed_angle(self, u, v):
204        """
205            signed angle between two vectors range [-pi, pi]
206        """
207        return atan2(u.x * v.y - u.y * v.x, u.x * v.x + u.y * v.y)
208
209    def delta_angle(self, last):
210        """
211            signed delta angle between end of line and start of this one
212            this value is object's a0 for segment = self
213        """
214        if last is None:
215            return self.angle
216        return self.signed_angle(last.straight(1, 1).v, self.straight(1, 0).v)
217
218    def normal(self, t=0):
219        """
220            2d Line perpendicular on plane xy
221            at position t in current segment
222            lie on the right side
223            p1
224            |--x
225            p0
226        """
227        return Line(self.lerp(t), self.cross_z)
228
229    def sized_normal(self, t, size):
230        """
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
235            p1
236            |--x
237            p0
238        """
239        return Line(self.lerp(t), size * self.cross_z.normalized())
240
241    def lerp(self, t):
242        """
243            3d interpolation
244        """
245        return self.p + self.v * t
246
247    def intersect(self, line):
248        """
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
254        """
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
261
262    def intersect_ext(self, line):
263        """
264            same as intersect, but return param t on both lines
265        """
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
275
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
281        """
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
289
290    def steps(self, len):
291        steps = max(1, round(self.length / len, 0))
292        return 1 / steps, int(steps)
293
294    def in_place_offset(self, offset):
295        """
296            Offset current line
297            offset > 0 on the right part
298        """
299        self.p += offset * self.cross_z.normalized()
300
301    def offset(self, offset):
302        """
303            Return a new line
304            offset > 0 on the right part
305        """
306        return Line(self.p + offset * self.cross_z.normalized(), self.v)
307
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)
315
316    def straight(self, length, t=1):
317        return Line(self.lerp(t), self.v.normalized() * length)
318
319    def translate(self, dp):
320        self.p += dp
321
322    def rotate(self, a):
323        """
324            Rotate segment ccw arroud p0
325        """
326        ca = cos(a)
327        sa = sin(a)
328        self.v = Matrix([
329            [ca, -sa],
330            [sa, ca]
331            ]) @ self.v
332        return self
333
334    def scale(self, length):
335        self.v = length * self.v.normalized()
336        return self
337
338    def tangeant_unit_vector(self, t):
339        return self.v.normalized()
340
341    def as_curve(self, context):
342        """
343            Draw Line with open gl in screen space
344            aka: coords are in pixels
345        """
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)
359
360    def make_offset(self, offset, last=None):
361        """
362            Return offset between last and self.
363            Adjust last and self start to match
364            intersection point
365        """
366        line = self.offset(offset)
367        if last is None:
368            return line
369
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
417
418        return line
419
420    @property
421    def pts(self):
422        return [self.p0.to_3d(), self.p1.to_3d()]
423
424
425class 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
431
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
454
455    def translate(self, dp):
456        self.c += dp
457
458
459class Arc(Circle):
460    """
461        Represent a 2d Arc
462        TODO:
463            make it possible to define an arc by start point end point and center
464    """
465    def __init__(self, c, radius, a0, da):
466        """
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
477        """
478        Circle.__init__(self, Vector(c).to_2d(), radius)
479        self.line = None
480        self.a0 = a0
481        self.da = da
482
483    @property
484    def angle(self):
485        """
486            angle of vector p0 p1
487        """
488        v = self.p1 - self.p0
489        return atan2(v.y, v.x)
490
491    @property
492    def ccw(self):
493        return self.da > 0
494
495    def signed_angle(self, u, v):
496        """
497            signed angle between two vectors
498        """
499        return atan2(u.x * v.y - u.y * v.x, u.x * v.x + u.y * v.y)
500
501    def delta_angle(self, last):
502        """
503            signed delta angle between end of line and start of this one
504            this value is object's a0 for segment = self
505        """
506        if last is None:
507            return self.a0
508        return self.signed_angle(last.straight(1, 1).v, self.straight(1, 0).v)
509
510    def scale_rot_matrix(self, u, v):
511        """
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
517        """
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]
527            ])
528
529    @property
530    def p0(self):
531        """
532            start point of arc
533        """
534        return self.lerp(0)
535
536    @property
537    def p1(self):
538        """
539            end point of arc
540        """
541        return self.lerp(1)
542
543    @p0.setter
544    def p0(self, p0):
545        """
546            rotate and scale arc so it intersect p0 p1
547            da is not affected
548        """
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)
557
558    @p1.setter
559    def p1(self, p1):
560        """
561            rotate and scale arc so it intersect p0 p1
562            da is not affected
563        """
564        p0 = self.p0
565        u = self.p1 - p0
566        v = p1 - p0
567
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)
574
575    @property
576    def length(self):
577        """
578            arc length
579        """
580        return self.r * abs(self.da)
581
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)
590
591    def normal(self, t=0):
592        """
593            Perpendicular line starting at t
594            always on the right side
595        """
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)
601
602    def sized_normal(self, t, size):
603        """
604            Perpendicular line starting at t and of a length size
605            on the right side when size > 0
606        """
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())
613
614    def lerp(self, t):
615        """
616            Interpolate along segment
617            t parameter [0, 1] where 0 is start of arc and 1 is end
618        """
619        a = self.a0 + t * self.da
620        return self.c + Vector((self.r * cos(a), self.r * sin(a)))
621
622    def steps(self, length):
623        """
624            Compute step count given desired step length
625        """
626        steps = max(1, round(self.length / length, 0))
627        return 1.0 / steps, int(steps)
628
629    def intersect_ext(self, line):
630        """
631            same as intersect, but return param t on both lines
632        """
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
638
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)
643
644    def as_lines(self, steps):
645        """
646            convert Arc to lines
647        """
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
655
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
663
664    def offset(self, offset):
665        """
666            Offset circle
667            offset > 0 on the right part
668        """
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)
674
675    def tangeant(self, t, length):
676        """
677            Tangent line so we are able to chain Circle and lines
678            Beware, counterpart on Line does return an Arc !
679        """
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)
688
689    def tangeant_unit_vector(self, t):
690        """
691            Return Tangent vector of length 1
692        """
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
700
701    def straight(self, length, t=1):
702        """
703            Return a tangent Line
704            Counterpart on Line also return a Line
705        """
706        return self.tangeant(t, length)
707
708    def point_sur_segment(self, pt):
709        """
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
715        """
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
721
722    def rotate(self, a):
723        """
724            Rotate center so we rotate ccw around p0
725        """
726        ca = cos(a)
727        sa = sin(a)
728        rM = Matrix([
729            [ca, -sa],
730            [sa, ca]
731            ])
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
737
738    # make offset for line / arc, arc / arc
739    def make_offset(self, offset, last=None):
740
741        line = self.offset(offset)
742
743        if last is None:
744            return line
745
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:
755
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)
763
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
810
811            # compute da of last
812            u = last.p0 - last.c
813            v = p0 - last.c
814            last.da = self.signed_angle(u, v)
815
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
821
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)]
828
829    def as_curve(self, context):
830        """
831            Draw 2d arc with open gl in screen space
832            aka: coords are in pixels
833        """
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)
847
848
849class Line3d(Line):
850    """
851        3d Line
852        mostly a gl enabled for future use in manipulators
853        coords are in world space
854    """
855    def __init__(self, p=None, v=None, p0=None, p1=None, z_axis=None):
856        """
857            Init by either
858            p: Vector or tuple origin
859            v: Vector or tuple size and direction
860            or
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
865        """
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))
879
880    @property
881    def p0(self):
882        return self.p
883
884    @property
885    def p1(self):
886        return self.p + self.v
887
888    @p0.setter
889    def p0(self, p0):
890        """
891            Note: setting p0
892            move p0 only
893        """
894        p1 = self.p1
895        self.p = Vector(p0).to_3d()
896        self.v = p1 - p0
897
898    @p1.setter
899    def p1(self, p1):
900        """
901            Note: setting p1
902            move p1 only
903        """
904        self.v = Vector(p1).to_3d() - self.p
905
906    @property
907    def cross_z(self):
908        """
909            3d Vector perpendicular on plane xy
910            lie on the right side
911            p1
912            |--x
913            p0
914        """
915        return self.v.cross(Vector((0, 0, 1)))
916
917    @property
918    def cross(self):
919        """
920            3d Vector perpendicular on plane defined by z_axis
921            lie on the right side
922            p1
923            |--x
924            p0
925        """
926        return self.v.cross(self.z_axis)
927
928    def normal(self, t=0):
929        """
930            3d Vector perpendicular on plane defined by z_axis
931            lie on the right side
932            p1
933            |--x
934            p0
935        """
936        n = Line3d()
937        n.p = self.lerp(t)
938        n.v = self.cross
939        return n
940
941    def sized_normal(self, t, size):
942        """
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
946            p1
947            |--x
948            p0
949        """
950        p = self.lerp(t)
951        v = size * self.cross.normalized()
952        return Line3d(p, v, z_axis=self.z_axis)
953
954    def offset(self, offset):
955        """
956            offset > 0 on the right part
957        """
958        return Line3d(self.p + offset * self.cross.normalized(), self.v)
959
960    # unless override, 2d methods should raise NotImplementedError
961    def intersect(self, line):
962        raise NotImplementedError
963
964    def point_sur_segment(self, pt):
965        raise NotImplementedError
966
967    def tangeant(self, t, da, radius):
968        raise NotImplementedError
969