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 #####
23 # ----------------------------------------------------------
24 # Author: Stephen Leger (s-leger)
26 # ----------------------------------------------------------
27 from mathutils
import Vector
, Matrix
28 from math
import sin
, cos
, pi
, atan2
, sqrt
, acos
30 # allow to draw parts with gl for debug puropses
31 from .archipack_gl
import GlBaseLine
34 class Projection(GlBaseLine
):
37 GlBaseLine
.__init
__(self
)
39 def proj_xy(self
, t
, next
=None):
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
46 return self
.normal(t
).v
.normalized(), 1
47 v0
= self
.normal(1).v
.normalized()
48 v1
= next
.normal(0).v
.normalized()
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):
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
62 return Vector((0, 1)), 1
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
70 dz
= dz0
/ self
.length
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))
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()
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
):
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
98 def __init__(self
, p
=None, v
=None, p0
=None, p1
=None):
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
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
117 self
.p
= Vector((0, 0))
118 self
.v
= Vector((0, 0))
123 return Line(self
.p
.copy(), self
.v
.copy())
131 return self
.p
+ self
.v
140 self
.p
= Vector(p0
).to_2d()
149 self
.v
= Vector(p1
).to_2d() - self
.p
163 return atan2(self
.v
.y
, self
.v
.x
)
170 def angle_normal(self
):
172 2d angle of perpendicular
173 lie on the right side
178 return atan2(-self
.v
.x
, self
.v
.y
)
182 return Line(self
.p
, -self
.v
)
186 return Line(self
.p
+ self
.v
, -self
.v
)
191 2d Vector perpendicular on plane xy
192 lie on the right side
197 return Vector((self
.v
.y
, -self
.v
.x
))
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
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
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
234 lie on the right side when size > 0
239 return Line(self
.lerp(t
), size
* self
.cross_z
.normalized())
245 return self
.p
+ self
.v
* t
247 def intersect(self
, line
):
249 2d intersection on plane xy
252 p: point of intersection
253 t: param t of intersection on current line
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
269 return False, 0, 0, 0
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
279 t: param t de l'intersection sur le segment courant
280 d: distance laterale perpendiculaire positif a droite
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
):
297 offset > 0 on the right part
299 self
.p
+= offset
* self
.cross_z
.normalized()
301 def offset(self
, offset
):
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
):
311 c
= p
+ radius
* self
.cross_z
.normalized()
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
):
324 Rotate segment ccw arroud p0
334 def scale(self
, length
):
335 self
.v
= length
* self
.v
.normalized()
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
352 spline
.points
.add(len(pts
) - 1)
353 for i
, p
in enumerate(pts
):
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
366 line
= self
.offset(offset
)
370 if hasattr(last
, "r"):
371 res
, d
, t
= line
.point_sur_segment(last
.c
)
372 c
= (last
.r
* last
.r
) - (d
* d
)
378 # center is past start of line
380 p0
= line
.lerp(t
) - line
.v
.normalized() * sqrt(c
)
382 p0
= line
.lerp(t
) + line
.v
.normalized() * sqrt(c
)
386 da
= self
.signed_angle(u
, v
)
399 # intersect line / line
409 # intersect past this segment end
410 # or before last segment start
411 # print("u:%s t:%s" % (u, t))
422 return [self
.p0
.to_3d(), self
.p1
.to_3d()]
425 class Circle(Projection
):
426 def __init__(self
, c
, radius
):
427 Projection
.__init
__(self
)
429 self
.r2
= radius
* radius
432 def intersect(self
, line
):
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
444 return True, line
.lerp(t
), t
450 if abs(t0
) < abs(t1
):
451 return True, line
.lerp(t0
), t0
453 return True, line
.lerp(t1
), t1
455 def translate(self
, dp
):
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
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
476 stored internally as radians
478 Circle
.__init
__(self
, Vector(c
).to_2d(), radius
)
486 angle of vector p0 p1
488 v
= self
.p1
- self
.p0
489 return atan2(v
.y
, v
.x
)
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
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
518 # signed angle old new vectors (rotation)
519 a
= self
.signed_angle(u
, v
)
521 scale
= v
.length
/ u
.length
524 return scale
, Matrix([
546 rotate and scale arc so it intersect p0 p1
549 u
= self
.p0
- self
.p1
551 scale
, rM
= self
.scale_rot_matrix(u
, v
)
552 self
.c
= self
.p1
+ rM
@ (self
.c
- self
.p1
)
554 self
.r2
= self
.r
* self
.r
556 self
.a0
= atan2(dp
.y
, dp
.x
)
561 rotate and scale arc so it intersect p0 p1
568 scale
, rM
= self
.scale_rot_matrix(u
, v
)
569 self
.c
= p0
+ rM
@ (self
.c
- p0
)
571 self
.r2
= self
.r
* self
.r
573 self
.a0
= atan2(dp
.y
, dp
.x
)
580 return self
.r
* abs(self
.da
)
584 a0
= self
.a0
+ self
.da
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
598 return Line(p
, self
.c
- p
)
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
612 return Line(p
, size
* v
.normalized())
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
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
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
):
650 for step
in range(steps
):
651 p1
= self
.lerp((step
+ 1) / steps
)
652 s
= Line(p0
=p0
, p1
=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
)
664 def offset(self
, offset
):
667 offset > 0 on the right part
670 radius
= self
.r
+ offset
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
683 p
= self
.c
+ Vector((self
.r
* ca
, self
.r
* sa
))
684 v
= Vector((length
* sa
, -length
* ca
))
689 def tangeant_unit_vector(self
, t
):
691 Return Tangent vector of length 1
693 a
= self
.a0
+ t
* self
.da
696 v
= Vector((sa
, -ca
))
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 ?
712 True when pt lie on segment
713 t [0, 1] where it lie (normalized between start and end)
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
724 Rotate center so we rotate ccw around p0
733 self
.c
= p0
+ rM
@ (self
.c
- p0
)
735 self
.a0
= atan2(dp
.y
, dp
.x
)
738 # make offset for line / arc, arc / arc
739 def make_offset(self
, offset
, last
=None):
741 line
= self
.offset(offset
)
746 if hasattr(last
, "v"):
747 # intersect line / arc
749 res
, d
, t
= last
.point_sur_segment(line
.c
)
750 c
= line
.r2
- (d
* d
)
756 # center is past end of line
758 # Arc take precedence
759 p0
= last
.lerp(t
) - last
.v
.normalized() * sqrt(c
)
761 # line take precedence
762 p0
= last
.lerp(t
) + last
.v
.normalized() * sqrt(c
)
764 # compute a0 and da of arc
767 line
.a0
= atan2(u
.y
, u
.x
)
768 da
= self
.signed_angle(u
, v
)
781 # intersect arc / arc x1 = self x0 = last
782 # rule to determine right side ->
783 # same side of d as p0 of self
785 tmp
= Line(last
.c
, dc
)
786 res
, d
, t
= tmp
.point_sur_segment(self
.p0
)
790 dist
< abs(last
.r
- self
.r
):
795 p0
= dc
* -last
.r
/ r
+ self
.c
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
)
803 res
, d1
, t
= tmp
.point_sur_segment(p0
)
804 # take other point if we are not on the same side
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
)
825 n_pts
= max(1, int(round(abs(self
.da
) / pi
* 30, 0)))
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
840 spline
.points
.add(len(pts
) - 1)
841 for i
, p
in enumerate(pts
):
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)
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):
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
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
873 self
.p
= Vector((0, 0, 0))
874 self
.v
= Vector((0, 0, 0))
875 if z_axis
is not None:
878 self
.z_axis
= Vector((0, 0, 1))
886 return self
.p
+ self
.v
895 self
.p
= Vector(p0
).to_3d()
904 self
.v
= Vector(p1
).to_3d() - self
.p
909 3d Vector perpendicular on plane xy
910 lie on the right side
915 return self
.v
.cross(Vector((0, 0, 1)))
920 3d Vector perpendicular on plane defined by z_axis
921 lie on the right side
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
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
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