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# Cutter / CutAble shared by roof, slab, and floor 26# ---------------------------------------------------------- 27from mathutils import Vector, Matrix 28from mathutils.geometry import interpolate_bezier 29from math import cos, sin, pi, atan2 30import bmesh 31from random import uniform 32from bpy.props import ( 33 FloatProperty, IntProperty, BoolProperty, 34 StringProperty, EnumProperty 35 ) 36from .archipack_2d import Line 37 38 39class CutterSegment(Line): 40 41 def __init__(self, p, v, type='DEFAULT'): 42 Line.__init__(self, p, v) 43 self.type = type 44 self.is_hole = True 45 46 @property 47 def copy(self): 48 return CutterSegment(self.p.copy(), self.v.copy(), self.type) 49 50 def straight(self, length, t=1): 51 s = self.copy 52 s.p = self.lerp(t) 53 s.v = self.v.normalized() * length 54 return s 55 56 def set_offset(self, offset, last=None): 57 """ 58 Offset line and compute intersection point 59 between segments 60 """ 61 self.line = self.make_offset(offset, last) 62 63 def offset(self, offset): 64 s = self.copy 65 s.p += self.sized_normal(0, offset).v 66 return s 67 68 @property 69 def oposite(self): 70 s = self.copy 71 s.p += s.v 72 s.v = -s.v 73 return s 74 75 76class CutterGenerator(): 77 78 def __init__(self, d): 79 self.parts = d.parts 80 self.operation = d.operation 81 self.segs = [] 82 83 def add_part(self, part): 84 85 if len(self.segs) < 1: 86 s = None 87 else: 88 s = self.segs[-1] 89 90 # start a new Cutter 91 if s is None: 92 v = part.length * Vector((cos(part.a0), sin(part.a0))) 93 s = CutterSegment(Vector((0, 0)), v, part.type) 94 else: 95 s = s.straight(part.length).rotate(part.a0) 96 s.type = part.type 97 98 self.segs.append(s) 99 100 def set_offset(self): 101 last = None 102 for i, seg in enumerate(self.segs): 103 seg.set_offset(self.parts[i].offset, last) 104 last = seg.line 105 106 def close(self): 107 # Make last segment implicit closing one 108 s0 = self.segs[-1] 109 s1 = self.segs[0] 110 dp = s1.p0 - s0.p0 111 s0.v = dp 112 113 if len(self.segs) > 1: 114 s0.line = s0.make_offset(self.parts[-1].offset, self.segs[-2].line) 115 116 p1 = s1.line.p1 117 s1.line = s1.make_offset(self.parts[0].offset, s0.line) 118 s1.line.p1 = p1 119 120 def locate_manipulators(self): 121 if self.operation == 'DIFFERENCE': 122 side = -1 123 else: 124 side = 1 125 for i, f in enumerate(self.segs): 126 127 manipulators = self.parts[i].manipulators 128 p0 = f.p0.to_3d() 129 p1 = f.p1.to_3d() 130 # angle from last to current segment 131 if i > 0: 132 133 if i < len(self.segs) - 1: 134 manipulators[0].type_key = 'ANGLE' 135 else: 136 manipulators[0].type_key = 'DUMB_ANGLE' 137 138 v0 = self.segs[i - 1].straight(-side, 1).v.to_3d() 139 v1 = f.straight(side, 0).v.to_3d() 140 manipulators[0].set_pts([p0, v0, v1]) 141 142 # segment length 143 manipulators[1].type_key = 'SIZE' 144 manipulators[1].prop1_name = "length" 145 manipulators[1].set_pts([p0, p1, (side, 0, 0)]) 146 147 # snap manipulator, don't change index ! 148 manipulators[2].set_pts([p0, p1, (side, 0, 0)]) 149 # dumb segment id 150 manipulators[3].set_pts([p0, p1, (side, 0, 0)]) 151 152 # offset 153 manipulators[4].set_pts([ 154 p0, 155 p0 + f.sized_normal(0, max(0.0001, self.parts[i].offset)).v.to_3d(), 156 (0.5, 0, 0) 157 ]) 158 159 def change_coordsys(self, fromTM, toTM): 160 """ 161 move shape fromTM into toTM coordsys 162 """ 163 dp = (toTM.inverted() @ fromTM.translation).to_2d() 164 da = toTM.row[1].to_2d().angle_signed(fromTM.row[1].to_2d()) 165 ca = cos(da) 166 sa = sin(da) 167 rM = Matrix([ 168 [ca, -sa], 169 [sa, ca] 170 ]) 171 for s in self.segs: 172 tp = (rM @ s.p0) - s.p0 + dp 173 s.rotate(da) 174 s.translate(tp) 175 176 def get_index(self, index): 177 n_segs = len(self.segs) 178 if index >= n_segs: 179 index -= n_segs 180 return index 181 182 def next_seg(self, index): 183 idx = self.get_index(index + 1) 184 return self.segs[idx] 185 186 def last_seg(self, index): 187 return self.segs[index - 1] 188 189 def get_verts(self, verts, edges): 190 191 n_segs = len(self.segs) - 1 192 193 for s in self.segs: 194 verts.append(s.line.p0.to_3d()) 195 196 for i in range(n_segs): 197 edges.append([i, i + 1]) 198 199 200class CutAblePolygon(): 201 """ 202 Simple boolean operations 203 Cutable generator / polygon 204 Object MUST have properties 205 - segs 206 - holes 207 - convex 208 """ 209 def as_lines(self, step_angle=0.104): 210 """ 211 Convert curved segments to straight lines 212 """ 213 segs = [] 214 for s in self.segs: 215 if "Curved" in type(s).__name__: 216 dt, steps = s.steps_by_angle(step_angle) 217 segs.extend(s.as_lines(steps)) 218 else: 219 segs.append(s) 220 self.segs = segs 221 222 def inside(self, pt, segs=None): 223 """ 224 Point inside poly (raycast method) 225 support concave polygons 226 TODO: 227 make s1 angle different than all othr segs 228 """ 229 s1 = Line(pt, Vector((min(10000, 100 * self.xsize), uniform(-0.5, 0.5)))) 230 counter = 0 231 if segs is None: 232 segs = self.segs 233 for s in segs: 234 res, p, t, u = s.intersect_ext(s1) 235 if res: 236 counter += 1 237 return counter % 2 == 1 238 239 def get_index(self, index): 240 n_segs = len(self.segs) 241 if index >= n_segs: 242 index -= n_segs 243 return index 244 245 def is_convex(self): 246 n_segs = len(self.segs) 247 self.convex = True 248 sign = False 249 s0 = self.segs[-1] 250 for i in range(n_segs): 251 s1 = self.segs[i] 252 if "Curved" in type(s1).__name__: 253 self.convex = False 254 return 255 c = s0.v.cross(s1.v) 256 if i == 0: 257 sign = (c > 0) 258 elif sign != (c > 0): 259 self.convex = False 260 return 261 s0 = s1 262 263 def get_intersections(self, border, cutter, s_start, segs, start_by_hole): 264 """ 265 Detect all intersections 266 for boundary: store intersection point, t, idx of segment, idx of cutter 267 sort by t 268 """ 269 s_segs = border.segs 270 b_segs = cutter.segs 271 s_nsegs = len(s_segs) 272 b_nsegs = len(b_segs) 273 inter = [] 274 275 # find all intersections 276 for idx in range(s_nsegs): 277 s_idx = border.get_index(s_start + idx) 278 s = s_segs[s_idx] 279 for b_idx, b in enumerate(b_segs): 280 res, p, u, v = s.intersect_ext(b) 281 if res: 282 inter.append((s_idx, u, b_idx, v, p)) 283 284 # print("%s" % (self.side)) 285 # print("%s" % (inter)) 286 287 if len(inter) < 1: 288 return True 289 290 # sort by seg and param t of seg 291 inter.sort() 292 293 # reorder so we really start from s_start 294 for i, it in enumerate(inter): 295 if it[0] >= s_start: 296 order = i 297 break 298 299 inter = inter[order:] + inter[:order] 300 301 # print("%s" % (inter)) 302 p0 = border.segs[s_start].p0 303 304 n_inter = len(inter) - 1 305 306 for i in range(n_inter): 307 s_end, u, b_start, v, p = inter[i] 308 s_idx = border.get_index(s_start) 309 s = s_segs[s_idx].copy 310 s.is_hole = not start_by_hole 311 segs.append(s) 312 idx = s_idx 313 max_iter = s_nsegs 314 # walk through s_segs until intersection 315 while s_idx != s_end and max_iter > 0: 316 idx += 1 317 s_idx = border.get_index(idx) 318 s = s_segs[s_idx].copy 319 s.is_hole = not start_by_hole 320 segs.append(s) 321 max_iter -= 1 322 segs[-1].p1 = p 323 324 s_start, u, b_end, v, p = inter[i + 1] 325 b_idx = cutter.get_index(b_start) 326 s = b_segs[b_idx].copy 327 s.is_hole = start_by_hole 328 segs.append(s) 329 idx = b_idx 330 max_iter = b_nsegs 331 # walk through b_segs until intersection 332 while b_idx != b_end and max_iter > 0: 333 idx += 1 334 b_idx = cutter.get_index(idx) 335 s = b_segs[b_idx].copy 336 s.is_hole = start_by_hole 337 segs.append(s) 338 max_iter -= 1 339 segs[-1].p1 = p 340 341 # add part between last intersection and start point 342 idx = s_start 343 s_idx = border.get_index(s_start) 344 s = s_segs[s_idx].copy 345 s.is_hole = not start_by_hole 346 segs.append(s) 347 max_iter = s_nsegs 348 # go until end of segment is near start of first one 349 while (s_segs[s_idx].p1 - p0).length > 0.0001 and max_iter > 0: 350 idx += 1 351 s_idx = border.get_index(idx) 352 s = s_segs[s_idx].copy 353 s.is_hole = not start_by_hole 354 segs.append(s) 355 max_iter -= 1 356 357 if len(segs) > s_nsegs + b_nsegs + 1: 358 # print("slice failed found:%s of:%s" % (len(segs), s_nsegs + b_nsegs)) 359 return False 360 361 for i, s in enumerate(segs): 362 s.p0 = segs[i - 1].p1 363 364 return True 365 366 def slice(self, cutter): 367 """ 368 Simple 2d Boolean between boundary and roof part 369 Doesn't handle slicing roof into multiple parts 370 371 4 cases: 372 1 pitch has point in boundary -> start from this point 373 2 boundary has point in pitch -> start from this point 374 3 no points inside -> find first crossing segment 375 4 not points inside and no crossing segments 376 """ 377 # print("************") 378 379 # keep inside or cut inside 380 # keep inside must be CCW 381 # cut inside must be CW 382 keep_inside = (cutter.operation == 'INTERSECTION') 383 384 start = -1 385 386 f_segs = self.segs 387 c_segs = cutter.segs 388 store = [] 389 390 slice_res = True 391 is_inside = False 392 393 # find if either a cutter or 394 # cutter intersects 395 # (at least one point of any must be inside other one) 396 397 # find a point of this pitch inside cutter 398 for i, s in enumerate(f_segs): 399 res = self.inside(s.p0, c_segs) 400 if res: 401 is_inside = True 402 if res == keep_inside: 403 start = i 404 # print("pitch pt %sside f_start:%s %s" % (in_out, start, self.side)) 405 slice_res = self.get_intersections(self, cutter, start, store, True) 406 break 407 408 # seek for point of cutter inside pitch 409 for i, s in enumerate(c_segs): 410 res = self.inside(s.p0) 411 if res: 412 is_inside = True 413 # no pitch point found inside cutter 414 if start < 0 and res == keep_inside: 415 start = i 416 # print("cutter pt %sside c_start:%s %s" % (in_out, start, self.side)) 417 # swap cutter / pitch so we start from cutter 418 slice_res = self.get_intersections(cutter, self, start, store, False) 419 break 420 421 # no points found at all 422 if start < 0: 423 # print("no pt inside") 424 return not keep_inside 425 426 if not slice_res: 427 # print("slice fails") 428 # found more segments than input 429 # cutter made more than one loop 430 return True 431 432 if len(store) < 1: 433 if is_inside: 434 # print("not touching, add as hole") 435 if keep_inside: 436 self.segs = cutter.segs 437 else: 438 self.holes.append(cutter) 439 440 return True 441 442 self.segs = store 443 self.is_convex() 444 445 return True 446 447 448class CutAbleGenerator(): 449 450 def bissect(self, bm, 451 plane_co, 452 plane_no, 453 dist=0.001, 454 use_snap_center=False, 455 clear_outer=True, 456 clear_inner=False 457 ): 458 geom = bm.verts[:] 459 geom.extend(bm.edges[:]) 460 geom.extend(bm.faces[:]) 461 462 bmesh.ops.bisect_plane(bm, 463 geom=geom, 464 dist=dist, 465 plane_co=plane_co, 466 plane_no=plane_no, 467 use_snap_center=False, 468 clear_outer=clear_outer, 469 clear_inner=clear_inner 470 ) 471 472 def cut_holes(self, bm, cutable, offset={'DEFAULT': 0}): 473 o_keys = offset.keys() 474 has_offset = len(o_keys) > 1 or offset['DEFAULT'] != 0 475 # cut holes 476 for hole in cutable.holes: 477 478 if has_offset: 479 480 for s in hole.segs: 481 if s.length > 0: 482 if s.type in o_keys: 483 of = offset[s.type] 484 else: 485 of = offset['DEFAULT'] 486 n = s.sized_normal(0, 1).v 487 p0 = s.p0 + n * of 488 self.bissect(bm, p0.to_3d(), n.to_3d(), clear_outer=False) 489 490 # compute boundary with offset 491 new_s = None 492 segs = [] 493 for s in hole.segs: 494 if s.length > 0: 495 if s.type in o_keys: 496 of = offset[s.type] 497 else: 498 of = offset['DEFAULT'] 499 new_s = s.make_offset(of, new_s) 500 segs.append(new_s) 501 # last / first intersection 502 if len(segs) > 0: 503 res, p0, t = segs[0].intersect(segs[-1]) 504 if res: 505 segs[0].p0 = p0 506 segs[-1].p1 = p0 507 508 else: 509 for s in hole.segs: 510 if s.length > 0: 511 n = s.sized_normal(0, 1).v 512 self.bissect(bm, s.p0.to_3d(), n.to_3d(), clear_outer=False) 513 # use hole boundary 514 segs = hole.segs 515 if len(segs) > 0: 516 # when hole segs are found clear parts inside hole 517 f_geom = [f for f in bm.faces 518 if cutable.inside( 519 f.calc_center_median().to_2d(), 520 segs=segs)] 521 if len(f_geom) > 0: 522 bmesh.ops.delete(bm, geom=f_geom, context='FACES') 523 524 def cut_boundary(self, bm, cutable, offset={'DEFAULT': 0}): 525 o_keys = offset.keys() 526 has_offset = len(o_keys) > 1 or offset['DEFAULT'] != 0 527 # cut outside parts 528 if has_offset: 529 for s in cutable.segs: 530 if s.length > 0: 531 if s.type in o_keys: 532 of = offset[s.type] 533 else: 534 of = offset['DEFAULT'] 535 n = s.sized_normal(0, 1).v 536 p0 = s.p0 + n * of 537 self.bissect(bm, p0.to_3d(), n.to_3d(), clear_outer=cutable.convex) 538 else: 539 for s in cutable.segs: 540 if s.length > 0: 541 n = s.sized_normal(0, 1).v 542 self.bissect(bm, s.p0.to_3d(), n.to_3d(), clear_outer=cutable.convex) 543 544 if not cutable.convex: 545 f_geom = [f for f in bm.faces 546 if not cutable.inside(f.calc_center_median().to_2d())] 547 if len(f_geom) > 0: 548 bmesh.ops.delete(bm, geom=f_geom, context='FACES') 549 550 551def update_hole(self, context): 552 # update parent's only when manipulated 553 self.update(context, update_parent=True) 554 555 556class ArchipackCutterPart(): 557 """ 558 Cutter segment PropertyGroup 559 560 Childs MUST implements 561 -find_in_selection 562 Childs MUST define 563 -type EnumProperty 564 """ 565 length : FloatProperty( 566 name="Length", 567 min=0.01, 568 max=1000.0, 569 default=2.0, 570 update=update_hole 571 ) 572 a0 : FloatProperty( 573 name="Angle", 574 min=-2 * pi, 575 max=2 * pi, 576 default=0, 577 subtype='ANGLE', unit='ROTATION', 578 update=update_hole 579 ) 580 offset : FloatProperty( 581 name="Offset", 582 min=0, 583 default=0, 584 update=update_hole 585 ) 586 587 def find_in_selection(self, context): 588 raise NotImplementedError 589 590 def draw(self, layout, context, index): 591 box = layout.box() 592 box.prop(self, "type", text=str(index + 1)) 593 box.prop(self, "length") 594 # box.prop(self, "offset") 595 box.prop(self, "a0") 596 597 def update(self, context, update_parent=False): 598 props = self.find_in_selection(context) 599 if props is not None: 600 props.update(context, update_parent=update_parent) 601 602 603def update_operation(self, context): 604 self.reverse(context, make_ccw=(self.operation == 'INTERSECTION')) 605 606 607def update_path(self, context): 608 self.update_path(context) 609 610 611def update(self, context): 612 self.update(context) 613 614 615def update_manipulators(self, context): 616 self.update(context, manipulable_refresh=True) 617 618 619class ArchipackCutter(): 620 n_parts : IntProperty( 621 name="Parts", 622 min=1, 623 default=1, update=update_manipulators 624 ) 625 z : FloatProperty( 626 name="dumb z", 627 description="Dumb z for manipulator placeholder", 628 default=0.01, 629 options={'SKIP_SAVE'} 630 ) 631 user_defined_path : StringProperty( 632 name="User defined", 633 update=update_path 634 ) 635 user_defined_resolution : IntProperty( 636 name="Resolution", 637 min=1, 638 max=128, 639 default=12, update=update_path 640 ) 641 operation : EnumProperty( 642 items=( 643 ('DIFFERENCE', 'Difference', 'Cut inside part', 0), 644 ('INTERSECTION', 'Intersection', 'Keep inside part', 1) 645 ), 646 default='DIFFERENCE', 647 update=update_operation 648 ) 649 auto_update : BoolProperty( 650 options={'SKIP_SAVE'}, 651 default=True, 652 update=update_manipulators 653 ) 654 # UI layout related 655 parts_expand : BoolProperty( 656 default=False 657 ) 658 659 closed = True 660 661 def draw(self, layout, context): 662 box = layout.box() 663 row = box.row() 664 if self.parts_expand: 665 row.prop(self, 'parts_expand', icon="TRIA_DOWN", text="Parts", emboss=False) 666 box.prop(self, 'n_parts') 667 for i, part in enumerate(self.parts): 668 part.draw(layout, context, i) 669 else: 670 row.prop(self, 'parts_expand', icon="TRIA_RIGHT", text="Parts", emboss=False) 671 672 def update_parts(self): 673 # print("update_parts") 674 # remove rows 675 # NOTE: 676 # n_parts+1 677 # as last one is end point of last segment or closing one 678 for i in range(len(self.parts), self.n_parts + 1, -1): 679 self.parts.remove(i - 1) 680 681 # add rows 682 for i in range(len(self.parts), self.n_parts + 1): 683 self.parts.add() 684 685 self.setup_manipulators() 686 687 def update_parent(self, context): 688 raise NotImplementedError 689 690 def setup_manipulators(self): 691 for i in range(self.n_parts + 1): 692 p = self.parts[i] 693 n_manips = len(p.manipulators) 694 if n_manips < 1: 695 s = p.manipulators.add() 696 s.type_key = "ANGLE" 697 s.prop1_name = "a0" 698 if n_manips < 2: 699 s = p.manipulators.add() 700 s.type_key = "SIZE" 701 s.prop1_name = "length" 702 if n_manips < 3: 703 s = p.manipulators.add() 704 s.type_key = 'WALL_SNAP' 705 s.prop1_name = str(i) 706 s.prop2_name = 'z' 707 if n_manips < 4: 708 s = p.manipulators.add() 709 s.type_key = 'DUMB_STRING' 710 s.prop1_name = str(i + 1) 711 if n_manips < 5: 712 s = p.manipulators.add() 713 s.type_key = "SIZE" 714 s.prop1_name = "offset" 715 p.manipulators[2].prop1_name = str(i) 716 p.manipulators[3].prop1_name = str(i + 1) 717 718 def get_generator(self): 719 g = CutterGenerator(self) 720 for i, part in enumerate(self.parts): 721 g.add_part(part) 722 g.set_offset() 723 g.close() 724 return g 725 726 def interpolate_bezier(self, pts, wM, p0, p1, resolution): 727 # straight segment, worth testing here 728 # since this can lower points count by a resolution factor 729 # use normalized to handle non linear t 730 if resolution == 0: 731 pts.append(wM @ p0.co.to_3d()) 732 else: 733 v = (p1.co - p0.co).normalized() 734 d1 = (p0.handle_right - p0.co).normalized() 735 d2 = (p1.co - p1.handle_left).normalized() 736 if d1 == v and d2 == v: 737 pts.append(wM @ p0.co.to_3d()) 738 else: 739 seg = interpolate_bezier(wM @ p0.co, 740 wM @ p0.handle_right, 741 wM @ p1.handle_left, 742 wM @ p1.co, 743 resolution + 1) 744 for i in range(resolution): 745 pts.append(seg[i].to_3d()) 746 747 def is_cw(self, pts): 748 p0 = pts[0] 749 d = 0 750 for p in pts[1:]: 751 d += (p.x * p0.y - p.y * p0.x) 752 p0 = p 753 return d > 0 754 755 def ensure_direction(self): 756 # get segs ensure they are cw or ccw depending on operation 757 # whatever the user do with points 758 g = self.get_generator() 759 pts = [seg.p0.to_3d() for seg in g.segs] 760 if self.is_cw(pts) != (self.operation == 'INTERSECTION'): 761 return g 762 g.segs = [s.oposite for s in reversed(g.segs)] 763 return g 764 765 def from_spline(self, context, wM, resolution, spline): 766 pts = [] 767 if spline.type == 'POLY': 768 pts = [wM @ p.co.to_3d() for p in spline.points] 769 if spline.use_cyclic_u: 770 pts.append(pts[0]) 771 elif spline.type == 'BEZIER': 772 points = spline.bezier_points 773 for i in range(1, len(points)): 774 p0 = points[i - 1] 775 p1 = points[i] 776 self.interpolate_bezier(pts, wM, p0, p1, resolution) 777 if spline.use_cyclic_u: 778 p0 = points[-1] 779 p1 = points[0] 780 self.interpolate_bezier(pts, wM, p0, p1, resolution) 781 pts.append(pts[0]) 782 else: 783 pts.append(wM @ points[-1].co) 784 785 if self.is_cw(pts) == (self.operation == 'INTERSECTION'): 786 pts = list(reversed(pts)) 787 788 pt = wM.inverted() @ pts[0] 789 790 # pretranslate 791 o = self.find_in_selection(context, self.auto_update) 792 o.matrix_world = wM @ Matrix.Translation(pt) 793 self.auto_update = False 794 self.from_points(pts) 795 self.auto_update = True 796 self.update_parent(context, o) 797 798 def from_points(self, pts): 799 800 self.n_parts = len(pts) - 2 801 802 self.update_parts() 803 804 p0 = pts.pop(0) 805 a0 = 0 806 for i, p1 in enumerate(pts): 807 dp = p1 - p0 808 da = atan2(dp.y, dp.x) - a0 809 if da > pi: 810 da -= 2 * pi 811 if da < -pi: 812 da += 2 * pi 813 if i >= len(self.parts): 814 # print("Too many pts for parts") 815 break 816 p = self.parts[i] 817 p.length = dp.to_2d().length 818 p.dz = dp.z 819 p.a0 = da 820 a0 += da 821 p0 = p1 822 823 def reverse(self, context, make_ccw=False): 824 825 o = self.find_in_selection(context, self.auto_update) 826 827 g = self.get_generator() 828 829 pts = [seg.p0.to_3d() for seg in g.segs] 830 831 if self.is_cw(pts) != make_ccw: 832 return 833 834 types = [p.type for p in self.parts] 835 836 pts.append(pts[0]) 837 838 pts = list(reversed(pts)) 839 self.auto_update = False 840 841 self.from_points(pts) 842 843 for i, type in enumerate(reversed(types)): 844 self.parts[i].type = type 845 self.auto_update = True 846 self.update_parent(context, o) 847 848 def update_path(self, context): 849 850 user_def_path = context.scene.objects.get(self.user_defined_path.strip()) 851 if user_def_path is not None and user_def_path.type == 'CURVE': 852 self.from_spline(context, 853 user_def_path.matrix_world, 854 self.user_defined_resolution, 855 user_def_path.data.splines[0]) 856 857 def make_surface(self, o, verts, edges): 858 bm = bmesh.new() 859 for v in verts: 860 bm.verts.new(v) 861 bm.verts.ensure_lookup_table() 862 for ed in edges: 863 bm.edges.new((bm.verts[ed[0]], bm.verts[ed[1]])) 864 bm.edges.new((bm.verts[-1], bm.verts[0])) 865 bm.edges.ensure_lookup_table() 866 bm.to_mesh(o.data) 867 bm.free() 868 869 def update(self, context, manipulable_refresh=False, update_parent=False): 870 871 o = self.find_in_selection(context, self.auto_update) 872 873 if o is None: 874 return 875 876 # clean up manipulators before any data model change 877 if manipulable_refresh: 878 self.manipulable_disable(context) 879 880 self.update_parts() 881 882 verts = [] 883 edges = [] 884 885 g = self.get_generator() 886 g.locate_manipulators() 887 888 # vertex index in order to build axis 889 g.get_verts(verts, edges) 890 891 if len(verts) > 2: 892 self.make_surface(o, verts, edges) 893 894 # enable manipulators rebuild 895 if manipulable_refresh: 896 self.manipulable_refresh = True 897 898 # update parent on direct edit 899 if manipulable_refresh or update_parent: 900 self.update_parent(context, o) 901 902 # restore context 903 self.restore_context(context) 904 905 def manipulable_setup(self, context): 906 907 self.manipulable_disable(context) 908 o = context.object 909 910 n_parts = self.n_parts + 1 911 912 self.setup_manipulators() 913 914 for i, part in enumerate(self.parts): 915 if i < n_parts: 916 917 if i > 0: 918 # start angle 919 self.manip_stack.append(part.manipulators[0].setup(context, o, part)) 920 921 # length 922 self.manip_stack.append(part.manipulators[1].setup(context, o, part)) 923 # index 924 self.manip_stack.append(part.manipulators[3].setup(context, o, self)) 925 # offset 926 # self.manip_stack.append(part.manipulators[4].setup(context, o, part)) 927 928 # snap point 929 self.manip_stack.append(part.manipulators[2].setup(context, o, self)) 930