Sun position: remove unused prop in HDRI mode
[blender-addons.git] / archipack / archipack_cutter.py
blobec43ad19d71aeb2a1aa6fccce74c4a3effd28fd6
1 # -*- coding:utf-8 -*-
3 # ##### BEGIN GPL LICENSE BLOCK #####
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software Foundation,
17 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
19 # ##### END GPL LICENSE BLOCK #####
21 # <pep8 compliant>
23 # ----------------------------------------------------------
24 # Author: Stephen Leger (s-leger)
25 # Cutter / CutAble shared by roof, slab, and floor
26 # ----------------------------------------------------------
27 from mathutils import Vector, Matrix
28 from mathutils.geometry import interpolate_bezier
29 from math import cos, sin, pi, atan2
30 import bmesh
31 from random import uniform
32 from bpy.props import (
33 FloatProperty, IntProperty, BoolProperty,
34 StringProperty, EnumProperty
36 from .archipack_2d import Line
39 class CutterSegment(Line):
41 def __init__(self, p, v, type='DEFAULT'):
42 Line.__init__(self, p, v)
43 self.type = type
44 self.is_hole = True
46 @property
47 def copy(self):
48 return CutterSegment(self.p.copy(), self.v.copy(), self.type)
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
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)
63 def offset(self, offset):
64 s = self.copy
65 s.p += self.sized_normal(0, offset).v
66 return s
68 @property
69 def oposite(self):
70 s = self.copy
71 s.p += s.v
72 s.v = -s.v
73 return s
76 class CutterGenerator():
78 def __init__(self, d):
79 self.parts = d.parts
80 self.operation = d.operation
81 self.segs = []
83 def add_part(self, part):
85 if len(self.segs) < 1:
86 s = None
87 else:
88 s = self.segs[-1]
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
98 self.segs.append(s)
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
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
113 if len(self.segs) > 1:
114 s0.line = s0.make_offset(self.parts[-1].offset, self.segs[-2].line)
116 p1 = s1.line.p1
117 s1.line = s1.make_offset(self.parts[0].offset, s0.line)
118 s1.line.p1 = p1
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):
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:
133 if i < len(self.segs) - 1:
134 manipulators[0].type_key = 'ANGLE'
135 else:
136 manipulators[0].type_key = 'DUMB_ANGLE'
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])
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)])
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)])
152 # offset
153 manipulators[4].set_pts([
155 p0 + f.sized_normal(0, max(0.0001, self.parts[i].offset)).v.to_3d(),
156 (0.5, 0, 0)
159 def change_coordsys(self, fromTM, toTM):
161 move shape fromTM into toTM coordsys
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]
171 for s in self.segs:
172 tp = (rM @ s.p0) - s.p0 + dp
173 s.rotate(da)
174 s.translate(tp)
176 def get_index(self, index):
177 n_segs = len(self.segs)
178 if index >= n_segs:
179 index -= n_segs
180 return index
182 def next_seg(self, index):
183 idx = self.get_index(index + 1)
184 return self.segs[idx]
186 def last_seg(self, index):
187 return self.segs[index - 1]
189 def get_verts(self, verts, edges):
191 n_segs = len(self.segs) - 1
193 for s in self.segs:
194 verts.append(s.line.p0.to_3d())
196 for i in range(n_segs):
197 edges.append([i, i + 1])
200 class CutAblePolygon():
202 Simple boolean operations
203 Cutable generator / polygon
204 Object MUST have properties
205 - segs
206 - holes
207 - convex
209 def as_lines(self, step_angle=0.104):
211 Convert curved segments to straight lines
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
222 def inside(self, pt, segs=None):
224 Point inside poly (raycast method)
225 support concave polygons
226 TODO:
227 make s1 angle different than all othr segs
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
239 def get_index(self, index):
240 n_segs = len(self.segs)
241 if index >= n_segs:
242 index -= n_segs
243 return index
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
263 def get_intersections(self, border, cutter, s_start, segs, start_by_hole):
265 Detect all intersections
266 for boundary: store intersection point, t, idx of segment, idx of cutter
267 sort by t
269 s_segs = border.segs
270 b_segs = cutter.segs
271 s_nsegs = len(s_segs)
272 b_nsegs = len(b_segs)
273 inter = []
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))
284 # print("%s" % (self.side))
285 # print("%s" % (inter))
287 if len(inter) < 1:
288 return True
290 # sort by seg and param t of seg
291 inter.sort()
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
299 inter = inter[order:] + inter[:order]
301 # print("%s" % (inter))
302 p0 = border.segs[s_start].p0
304 n_inter = len(inter) - 1
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
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
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
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
361 for i, s in enumerate(segs):
362 s.p0 = segs[i - 1].p1
364 return True
366 def slice(self, cutter):
368 Simple 2d Boolean between boundary and roof part
369 Doesn't handle slicing roof into multiple parts
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
377 # print("************")
379 # keep inside or cut inside
380 # keep inside must be CCW
381 # cut inside must be CW
382 keep_inside = (cutter.operation == 'INTERSECTION')
384 start = -1
386 f_segs = self.segs
387 c_segs = cutter.segs
388 store = []
390 slice_res = True
391 is_inside = False
393 # find if either a cutter or
394 # cutter intersects
395 # (at least one point of any must be inside other one)
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
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
421 # no points found at all
422 if start < 0:
423 # print("no pt inside")
424 return not keep_inside
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
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)
440 return True
442 self.segs = store
443 self.is_convex()
445 return True
448 class CutAbleGenerator():
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
458 geom = bm.verts[:]
459 geom.extend(bm.edges[:])
460 geom.extend(bm.faces[:])
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
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:
478 if has_offset:
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)
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
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')
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)
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')
551 def update_hole(self, context):
552 # update parent's only when manipulated
553 self.update(context, update_parent=True)
556 class ArchipackCutterPart():
558 Cutter segment PropertyGroup
560 Childs MUST implements
561 -find_in_selection
562 Childs MUST define
563 -type EnumProperty
565 length : FloatProperty(
566 name="Length",
567 min=0.01,
568 max=1000.0,
569 default=2.0,
570 update=update_hole
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
580 offset : FloatProperty(
581 name="Offset",
582 min=0,
583 default=0,
584 update=update_hole
587 def find_in_selection(self, context):
588 raise NotImplementedError
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")
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)
603 def update_operation(self, context):
604 self.reverse(context, make_ccw=(self.operation == 'INTERSECTION'))
607 def update_path(self, context):
608 self.update_path(context)
611 def update(self, context):
612 self.update(context)
615 def update_manipulators(self, context):
616 self.update(context, manipulable_refresh=True)
619 class ArchipackCutter():
620 n_parts : IntProperty(
621 name="Parts",
622 min=1,
623 default=1, update=update_manipulators
625 z : FloatProperty(
626 name="dumb z",
627 description="Dumb z for manipulator placeholder",
628 default=0.01,
629 options={'SKIP_SAVE'}
631 user_defined_path : StringProperty(
632 name="User defined",
633 update=update_path
635 user_defined_resolution : IntProperty(
636 name="Resolution",
637 min=1,
638 max=128,
639 default=12, update=update_path
641 operation : EnumProperty(
642 items=(
643 ('DIFFERENCE', 'Difference', 'Cut inside part', 0),
644 ('INTERSECTION', 'Intersection', 'Keep inside part', 1)
646 default='DIFFERENCE',
647 update=update_operation
649 auto_update : BoolProperty(
650 options={'SKIP_SAVE'},
651 default=True,
652 update=update_manipulators
654 # UI layout related
655 parts_expand : BoolProperty(
656 default=False
659 closed = True
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)
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)
681 # add rows
682 for i in range(len(self.parts), self.n_parts + 1):
683 self.parts.add()
685 self.setup_manipulators()
687 def update_parent(self, context):
688 raise NotImplementedError
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)
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
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())
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
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
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)
785 if self.is_cw(pts) == (self.operation == 'INTERSECTION'):
786 pts = list(reversed(pts))
788 pt = wM.inverted() @ pts[0]
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)
798 def from_points(self, pts):
800 self.n_parts = len(pts) - 2
802 self.update_parts()
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
823 def reverse(self, context, make_ccw=False):
825 o = self.find_in_selection(context, self.auto_update)
827 g = self.get_generator()
829 pts = [seg.p0.to_3d() for seg in g.segs]
831 if self.is_cw(pts) != make_ccw:
832 return
834 types = [p.type for p in self.parts]
836 pts.append(pts[0])
838 pts = list(reversed(pts))
839 self.auto_update = False
841 self.from_points(pts)
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)
848 def update_path(self, context):
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])
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()
869 def update(self, context, manipulable_refresh=False, update_parent=False):
871 o = self.find_in_selection(context, self.auto_update)
873 if o is None:
874 return
876 # clean up manipulators before any data model change
877 if manipulable_refresh:
878 self.manipulable_disable(context)
880 self.update_parts()
882 verts = []
883 edges = []
885 g = self.get_generator()
886 g.locate_manipulators()
888 # vertex index in order to build axis
889 g.get_verts(verts, edges)
891 if len(verts) > 2:
892 self.make_surface(o, verts, edges)
894 # enable manipulators rebuild
895 if manipulable_refresh:
896 self.manipulable_refresh = True
898 # update parent on direct edit
899 if manipulable_refresh or update_parent:
900 self.update_parent(context, o)
902 # restore context
903 self.restore_context(context)
905 def manipulable_setup(self, context):
907 self.manipulable_disable(context)
908 o = context.object
910 n_parts = self.n_parts + 1
912 self.setup_manipulators()
914 for i, part in enumerate(self.parts):
915 if i < n_parts:
917 if i > 0:
918 # start angle
919 self.manip_stack.append(part.manipulators[0].setup(context, o, part))
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))
928 # snap point
929 self.manip_stack.append(part.manipulators[2].setup(context, o, self))