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