Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'archipack/archipack_cutter.py')
-rw-r--r--archipack/archipack_cutter.py910
1 files changed, 910 insertions, 0 deletions
diff --git a/archipack/archipack_cutter.py b/archipack/archipack_cutter.py
new file mode 100644
index 00000000..bce82008
--- /dev/null
+++ b/archipack/archipack_cutter.py
@@ -0,0 +1,910 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# Author: Stephen Leger (s-leger)
+# Cutter / CutAble shared by roof, slab, and floor
+# ----------------------------------------------------------
+from mathutils import Vector, Matrix
+from mathutils.geometry import interpolate_bezier
+from math import cos, sin, pi, atan2
+import bmesh
+from bpy.props import (
+ FloatProperty, IntProperty, BoolProperty,
+ StringProperty, EnumProperty
+ )
+from .archipack_2d import Line
+
+
+class CutterSegment(Line):
+
+ def __init__(self, p, v, type='DEFAULT'):
+ Line.__init__(self, p, v)
+ self.type = type
+
+ @property
+ def copy(self):
+ return CutterSegment(self.p.copy(), self.v.copy(), self.type)
+
+ def straight(self, length, t=1):
+ s = self.copy
+ s.p = self.lerp(t)
+ s.v = self.v.normalized() * length
+ return s
+
+ def set_offset(self, offset, last=None):
+ """
+ Offset line and compute intersection point
+ between segments
+ """
+ self.line = self.make_offset(offset, last)
+
+ def offset(self, offset):
+ s = self.copy
+ s.p += offset * self.cross_z.normalized()
+ return s
+
+ @property
+ def oposite(self):
+ s = self.copy
+ s.p += s.v
+ s.v = -s.v
+ return s
+
+
+class CutterGenerator():
+
+ def __init__(self, d):
+ self.parts = d.parts
+ self.operation = d.operation
+ self.segs = []
+
+ def add_part(self, part):
+
+ if len(self.segs) < 1:
+ s = None
+ else:
+ s = self.segs[-1]
+
+ # start a new Cutter
+ if s is None:
+ v = part.length * Vector((cos(part.a0), sin(part.a0)))
+ s = CutterSegment(Vector((0, 0)), v, part.type)
+ else:
+ s = s.straight(part.length).rotate(part.a0)
+ s.type = part.type
+
+ self.segs.append(s)
+
+ def set_offset(self):
+ last = None
+ for i, seg in enumerate(self.segs):
+ seg.set_offset(self.parts[i].offset, last)
+ last = seg.line
+
+ def close(self):
+ # Make last segment implicit closing one
+ s0 = self.segs[-1]
+ s1 = self.segs[0]
+ dp = s1.p0 - s0.p0
+ s0.v = dp
+
+ if len(self.segs) > 1:
+ s0.line = s0.make_offset(self.parts[-1].offset, self.segs[-2].line)
+
+ p1 = s1.line.p1
+ s1.line = s1.make_offset(self.parts[0].offset, s0.line)
+ s1.line.p1 = p1
+
+ def locate_manipulators(self):
+ if self.operation == 'DIFFERENCE':
+ side = -1
+ else:
+ side = 1
+ for i, f in enumerate(self.segs):
+
+ manipulators = self.parts[i].manipulators
+ p0 = f.p0.to_3d()
+ p1 = f.p1.to_3d()
+ # angle from last to current segment
+ if i > 0:
+
+ if i < len(self.segs) - 1:
+ manipulators[0].type_key = 'ANGLE'
+ else:
+ manipulators[0].type_key = 'DUMB_ANGLE'
+
+ v0 = self.segs[i - 1].straight(-side, 1).v.to_3d()
+ v1 = f.straight(side, 0).v.to_3d()
+ manipulators[0].set_pts([p0, v0, v1])
+
+ # segment length
+ manipulators[1].type_key = 'SIZE'
+ manipulators[1].prop1_name = "length"
+ manipulators[1].set_pts([p0, p1, (side, 0, 0)])
+
+ # snap manipulator, dont change index !
+ manipulators[2].set_pts([p0, p1, (side, 0, 0)])
+ # dumb segment id
+ manipulators[3].set_pts([p0, p1, (side, 0, 0)])
+
+ # offset
+ manipulators[4].set_pts([
+ p0,
+ p0 + f.sized_normal(0, max(0.0001, self.parts[i].offset)).v.to_3d(),
+ (0.5, 0, 0)
+ ])
+
+ def change_coordsys(self, fromTM, toTM):
+ """
+ move shape fromTM into toTM coordsys
+ """
+ dp = (toTM.inverted() * fromTM.translation).to_2d()
+ da = toTM.row[1].to_2d().angle_signed(fromTM.row[1].to_2d())
+ ca = cos(da)
+ sa = sin(da)
+ rM = Matrix([
+ [ca, -sa],
+ [sa, ca]
+ ])
+ for s in self.segs:
+ tp = (rM * s.p0) - s.p0 + dp
+ s.rotate(da)
+ s.translate(tp)
+
+ def get_index(self, index):
+ n_segs = len(self.segs)
+ if index >= n_segs:
+ index -= n_segs
+ return index
+
+ def next_seg(self, index):
+ idx = self.get_index(index + 1)
+ return self.segs[idx]
+
+ def last_seg(self, index):
+ return self.segs[index - 1]
+
+ def get_verts(self, verts, edges):
+
+ n_segs = len(self.segs) - 1
+
+ for s in self.segs:
+ verts.append(s.line.p0.to_3d())
+
+ for i in range(n_segs):
+ edges.append([i, i + 1])
+
+
+class CutAblePolygon():
+ """
+ Simple boolean operations
+ Cutable generator / polygon
+ Object MUST have properties
+ - segs
+ - holes
+ - convex
+ """
+
+ def inside(self, pt, segs=None):
+ """
+ Point inside poly (raycast method)
+ support concave polygons
+ TODO:
+ make s1 angle different than all othr segs
+ """
+ s1 = Line(pt, Vector((100 * self.xsize, 0.1)))
+ counter = 0
+ if segs is None:
+ segs = self.segs
+ for s in segs:
+ res, p, t, u = s.intersect_ext(s1)
+ if res:
+ counter += 1
+ return counter % 2 == 1
+
+ def get_index(self, index):
+ n_segs = len(self.segs)
+ if index >= n_segs:
+ index -= n_segs
+ return index
+
+ def is_convex(self):
+ n_segs = len(self.segs)
+ self.convex = True
+ sign = False
+ s0 = self.segs[-1]
+ for i in range(n_segs):
+ s1 = self.segs[i]
+ c = s0.v.cross(s1.v)
+ if i == 0:
+ sign = (c > 0)
+ elif sign != (c > 0):
+ self.convex = False
+ return
+ s0 = s1
+
+ def get_intersections(self, border, cutter, s_start, segs, start_by_hole):
+ """
+ Detect all intersections
+ for boundary: store intersection point, t, idx of segment, idx of cutter
+ sort by t
+ """
+ s_segs = border.segs
+ b_segs = cutter.segs
+ s_nsegs = len(s_segs)
+ b_nsegs = len(b_segs)
+ inter = []
+
+ # find all intersections
+ for idx in range(s_nsegs):
+ s_idx = border.get_index(s_start + idx)
+ s = s_segs[s_idx]
+ for b_idx, b in enumerate(b_segs):
+ res, p, u, v = s.intersect_ext(b)
+ if res:
+ inter.append((s_idx, u, b_idx, v, p))
+
+ # print("%s" % (self.side))
+ # print("%s" % (inter))
+
+ if len(inter) < 1:
+ return True
+
+ # sort by seg and param t of seg
+ inter.sort()
+
+ # reorder so we realy start from s_start
+ for i, it in enumerate(inter):
+ if it[0] >= s_start:
+ order = i
+ break
+
+ inter = inter[order:] + inter[:order]
+
+ # print("%s" % (inter))
+ p0 = border.segs[s_start].p0
+
+ n_inter = len(inter) - 1
+
+ for i in range(n_inter):
+ s_end, u, b_start, v, p = inter[i]
+ s_idx = border.get_index(s_start)
+ s = s_segs[s_idx].copy
+ s.is_hole = not start_by_hole
+ segs.append(s)
+ idx = s_idx
+ max_iter = s_nsegs
+ # walk through s_segs until intersection
+ while s_idx != s_end and max_iter > 0:
+ idx += 1
+ s_idx = border.get_index(idx)
+ s = s_segs[s_idx].copy
+ s.is_hole = not start_by_hole
+ segs.append(s)
+ max_iter -= 1
+ segs[-1].p1 = p
+
+ s_start, u, b_end, v, p = inter[i + 1]
+ b_idx = cutter.get_index(b_start)
+ s = b_segs[b_idx].copy
+ s.is_hole = start_by_hole
+ segs.append(s)
+ idx = b_idx
+ max_iter = b_nsegs
+ # walk through b_segs until intersection
+ while b_idx != b_end and max_iter > 0:
+ idx += 1
+ b_idx = cutter.get_index(idx)
+ s = b_segs[b_idx].copy
+ s.is_hole = start_by_hole
+ segs.append(s)
+ max_iter -= 1
+ segs[-1].p1 = p
+
+ # add part between last intersection and start point
+ idx = s_start
+ s_idx = border.get_index(s_start)
+ s = s_segs[s_idx].copy
+ s.is_hole = not start_by_hole
+ segs.append(s)
+ max_iter = s_nsegs
+ # go until end of segment is near start of first one
+ while (s_segs[s_idx].p1 - p0).length > 0.0001 and max_iter > 0:
+ idx += 1
+ s_idx = border.get_index(idx)
+ s = s_segs[s_idx].copy
+ s.is_hole = not start_by_hole
+ segs.append(s)
+ max_iter -= 1
+
+ if len(segs) > s_nsegs + b_nsegs + 1:
+ # print("slice failed found:%s of:%s" % (len(segs), s_nsegs + b_nsegs))
+ return False
+
+ for i, s in enumerate(segs):
+ s.p0 = segs[i - 1].p1
+
+ return True
+
+ def slice(self, cutter):
+ """
+ Simple 2d Boolean between boundary and roof part
+ Dosen't handle slicing roof into multiple parts
+
+ 4 cases:
+ 1 pitch has point in boundary -> start from this point
+ 2 boundary has point in pitch -> start from this point
+ 3 no points inside -> find first crossing segment
+ 4 not points inside and no crossing segments
+ """
+ # print("************")
+
+ # keep inside or cut inside
+ # keep inside must be CCW
+ # cut inside must be CW
+ keep_inside = (cutter.operation == 'INTERSECTION')
+
+ start = -1
+
+ f_segs = self.segs
+ c_segs = cutter.segs
+ store = []
+
+ slice_res = True
+ is_inside = False
+
+ # find if either a cutter or
+ # cutter intersects
+ # (at least one point of any must be inside other one)
+
+ # find a point of this pitch inside cutter
+ for i, s in enumerate(f_segs):
+ res = self.inside(s.p0, c_segs)
+ if res:
+ is_inside = True
+ if res == keep_inside:
+ start = i
+ # print("pitch pt %sside f_start:%s %s" % (in_out, start, self.side))
+ slice_res = self.get_intersections(self, cutter, start, store, True)
+ break
+
+ # seek for point of cutter inside pitch
+ for i, s in enumerate(c_segs):
+ res = self.inside(s.p0)
+ if res:
+ is_inside = True
+ # no pitch point found inside cutter
+ if start < 0 and res == keep_inside:
+ start = i
+ # print("cutter pt %sside c_start:%s %s" % (in_out, start, self.side))
+ # swap cutter / pitch so we start from cutter
+ slice_res = self.get_intersections(cutter, self, start, store, False)
+ break
+
+ # no points found at all
+ if start < 0:
+ # print("no pt inside")
+ return
+
+ if not slice_res:
+ # print("slice fails")
+ # found more segments than input
+ # cutter made more than one loop
+ return
+
+ if len(store) < 1:
+ if is_inside:
+ # print("not touching, add as hole")
+ self.holes.append(cutter)
+ return
+
+ self.segs = store
+ self.is_convex()
+
+
+class CutAbleGenerator():
+
+ def bissect(self, bm,
+ plane_co,
+ plane_no,
+ dist=0.001,
+ use_snap_center=False,
+ clear_outer=True,
+ clear_inner=False
+ ):
+ geom = bm.verts[:]
+ geom.extend(bm.edges[:])
+ geom.extend(bm.faces[:])
+
+ bmesh.ops.bisect_plane(bm,
+ geom=geom,
+ dist=dist,
+ plane_co=plane_co,
+ plane_no=plane_no,
+ use_snap_center=False,
+ clear_outer=clear_outer,
+ clear_inner=clear_inner
+ )
+
+ def cut_holes(self, bm, cutable, offset={'DEFAULT': 0}):
+ o_keys = offset.keys()
+ has_offset = len(o_keys) > 1 or offset['DEFAULT'] != 0
+ # cut holes
+ for hole in cutable.holes:
+
+ if has_offset:
+
+ for s in hole.segs:
+ if s.length > 0:
+ if s.type in o_keys:
+ of = offset[s.type]
+ else:
+ of = offset['DEFAULT']
+ p0 = s.p0 + s.cross_z.normalized() * of
+ self.bissect(bm, p0.to_3d(), s.cross_z.to_3d(), clear_outer=False)
+
+ # compute boundary with offset
+ new_s = None
+ segs = []
+ for s in hole.segs:
+ if s.length > 0:
+ if s.type in o_keys:
+ of = offset[s.type]
+ else:
+ of = offset['DEFAULT']
+ new_s = s.make_offset(of, new_s)
+ segs.append(new_s)
+ # last / first intersection
+ if len(segs) > 0:
+ res, p0, t = segs[0].intersect(segs[-1])
+ if res:
+ segs[0].p0 = p0
+ segs[-1].p1 = p0
+
+ else:
+ for s in hole.segs:
+ if s.length > 0:
+ self.bissect(bm, s.p0.to_3d(), s.cross_z.to_3d(), clear_outer=False)
+ # use hole boundary
+ segs = hole.segs
+ if len(segs) > 0:
+ # when hole segs are found clear parts inside hole
+ f_geom = [f for f in bm.faces
+ if cutable.inside(
+ f.calc_center_median().to_2d(),
+ segs=segs)]
+ if len(f_geom) > 0:
+ bmesh.ops.delete(bm, geom=f_geom, context=5)
+
+ def cut_boundary(self, bm, cutable, offset={'DEFAULT': 0}):
+ o_keys = offset.keys()
+ has_offset = len(o_keys) > 1 or offset['DEFAULT'] != 0
+ # cut outside parts
+ if has_offset:
+ for s in cutable.segs:
+ if s.length > 0:
+ if s.type in o_keys:
+ of = offset[s.type]
+ else:
+ of = offset['DEFAULT']
+ p0 = s.p0 + s.cross_z.normalized() * of
+ self.bissect(bm, p0.to_3d(), s.cross_z.to_3d(), clear_outer=cutable.convex)
+ else:
+ for s in cutable.segs:
+ if s.length > 0:
+ self.bissect(bm, s.p0.to_3d(), s.cross_z.to_3d(), clear_outer=cutable.convex)
+
+ if not cutable.convex:
+ f_geom = [f for f in bm.faces
+ if not cutable.inside(f.calc_center_median().to_2d())]
+ if len(f_geom) > 0:
+ bmesh.ops.delete(bm, geom=f_geom, context=5)
+
+
+def update_hole(self, context):
+ # update parent's only when manipulated
+ self.update(context, update_parent=True)
+
+
+class ArchipackCutterPart():
+ """
+ Cutter segment PropertyGroup
+
+ Childs MUST implements
+ -find_in_selection
+ Childs MUST define
+ -type EnumProperty
+ """
+ length = FloatProperty(
+ name="length",
+ min=0.01,
+ max=1000.0,
+ default=2.0,
+ update=update_hole
+ )
+ a0 = FloatProperty(
+ name="angle",
+ min=-2 * pi,
+ max=2 * pi,
+ default=0,
+ subtype='ANGLE', unit='ROTATION',
+ update=update_hole
+ )
+ offset = FloatProperty(
+ name="offset",
+ min=0,
+ default=0,
+ update=update_hole
+ )
+
+ def find_in_selection(self, context):
+ raise NotImplementedError
+
+ def draw(self, layout, context, index):
+ box = layout.box()
+ box.prop(self, "type", text=str(index + 1))
+ box.prop(self, "length")
+ # box.prop(self, "offset")
+ box.prop(self, "a0")
+
+ def update(self, context, update_parent=False):
+ props = self.find_in_selection(context)
+ if props is not None:
+ props.update(context, update_parent=update_parent)
+
+
+def update_operation(self, context):
+ self.reverse(context, make_ccw=(self.operation == 'INTERSECTION'))
+
+
+def update_path(self, context):
+ self.update_path(context)
+
+
+def update(self, context):
+ self.update(context)
+
+
+def update_manipulators(self, context):
+ self.update(context, manipulable_refresh=True)
+
+
+class ArchipackCutter():
+ n_parts = IntProperty(
+ name="parts",
+ min=1,
+ default=1, update=update_manipulators
+ )
+ z = FloatProperty(
+ name="dumb z",
+ description="Dumb z for manipulator placeholder",
+ default=0.01,
+ options={'SKIP_SAVE'}
+ )
+ user_defined_path = StringProperty(
+ name="user defined",
+ update=update_path
+ )
+ user_defined_resolution = IntProperty(
+ name="resolution",
+ min=1,
+ max=128,
+ default=12, update=update_path
+ )
+ operation = EnumProperty(
+ items=(
+ ('DIFFERENCE', 'Difference', 'Cut inside part', 0),
+ ('INTERSECTION', 'Intersection', 'Keep inside part', 1)
+ ),
+ default='DIFFERENCE',
+ update=update_operation
+ )
+ auto_update = BoolProperty(
+ options={'SKIP_SAVE'},
+ default=True,
+ update=update_manipulators
+ )
+ # UI layout related
+ parts_expand = BoolProperty(
+ default=False
+ )
+ closed = BoolProperty(
+ description="keep closed to be wall snap manipulator compatible",
+ options={'SKIP_SAVE'},
+ default=True
+ )
+
+ def draw(self, layout, context):
+ box = layout.box()
+ row = box.row()
+ if self.parts_expand:
+ row.prop(self, 'parts_expand', icon="TRIA_DOWN", icon_only=True, text="Parts", emboss=False)
+ box.prop(self, 'n_parts')
+ for i, part in enumerate(self.parts):
+ if i < self.n_parts:
+ part.draw(layout, context, i)
+ else:
+ row.prop(self, 'parts_expand', icon="TRIA_RIGHT", icon_only=True, text="Parts", emboss=False)
+
+ def update_parts(self):
+ # print("update_parts")
+ # remove rows
+ # NOTE:
+ # n_parts+1
+ # as last one is end point of last segment or closing one
+ for i in range(len(self.parts), self.n_parts + 1, -1):
+ self.parts.remove(i - 1)
+
+ # add rows
+ for i in range(len(self.parts), self.n_parts + 1):
+ self.parts.add()
+
+ self.setup_manipulators()
+
+ def update_parent(self, context):
+ raise NotImplementedError
+
+ def setup_manipulators(self):
+ for i in range(self.n_parts + 1):
+ p = self.parts[i]
+ n_manips = len(p.manipulators)
+ if n_manips < 1:
+ s = p.manipulators.add()
+ s.type_key = "ANGLE"
+ s.prop1_name = "a0"
+ if n_manips < 2:
+ s = p.manipulators.add()
+ s.type_key = "SIZE"
+ s.prop1_name = "length"
+ if n_manips < 3:
+ s = p.manipulators.add()
+ s.type_key = 'WALL_SNAP'
+ s.prop1_name = str(i)
+ s.prop2_name = 'z'
+ if n_manips < 4:
+ s = p.manipulators.add()
+ s.type_key = 'DUMB_STRING'
+ s.prop1_name = str(i + 1)
+ if n_manips < 5:
+ s = p.manipulators.add()
+ s.type_key = "SIZE"
+ s.prop1_name = "offset"
+ p.manipulators[2].prop1_name = str(i)
+ p.manipulators[3].prop1_name = str(i + 1)
+
+ def get_generator(self):
+ g = CutterGenerator(self)
+ for i, part in enumerate(self.parts):
+ g.add_part(part)
+ g.set_offset()
+ g.close()
+ return g
+
+ def interpolate_bezier(self, pts, wM, p0, p1, resolution):
+ # straight segment, worth testing here
+ # since this can lower points count by a resolution factor
+ # use normalized to handle non linear t
+ if resolution == 0:
+ pts.append(wM * p0.co.to_3d())
+ else:
+ v = (p1.co - p0.co).normalized()
+ d1 = (p0.handle_right - p0.co).normalized()
+ d2 = (p1.co - p1.handle_left).normalized()
+ if d1 == v and d2 == v:
+ pts.append(wM * p0.co.to_3d())
+ else:
+ seg = interpolate_bezier(wM * p0.co,
+ wM * p0.handle_right,
+ wM * p1.handle_left,
+ wM * p1.co,
+ resolution + 1)
+ for i in range(resolution):
+ pts.append(seg[i].to_3d())
+
+ def is_cw(self, pts):
+ p0 = pts[0]
+ d = 0
+ for p in pts[1:]:
+ d += (p.x * p0.y - p.y * p0.x)
+ p0 = p
+ return d > 0
+
+ def ensure_direction(self):
+ # get segs ensure they are cw or ccw depending on operation
+ # whatever the user do with points
+ g = self.get_generator()
+ pts = [seg.p0.to_3d() for seg in g.segs]
+ if self.is_cw(pts) != (self.operation == 'INTERSECTION'):
+ return g
+ g.segs = [s.oposite for s in reversed(g.segs)]
+ return g
+
+ def from_spline(self, context, wM, resolution, spline):
+ pts = []
+ if spline.type == 'POLY':
+ pts = [wM * p.co.to_3d() for p in spline.points]
+ if spline.use_cyclic_u:
+ pts.append(pts[0])
+ elif spline.type == 'BEZIER':
+ points = spline.bezier_points
+ for i in range(1, len(points)):
+ p0 = points[i - 1]
+ p1 = points[i]
+ self.interpolate_bezier(pts, wM, p0, p1, resolution)
+ if spline.use_cyclic_u:
+ p0 = points[-1]
+ p1 = points[0]
+ self.interpolate_bezier(pts, wM, p0, p1, resolution)
+ pts.append(pts[0])
+ else:
+ pts.append(wM * points[-1].co)
+
+ if self.is_cw(pts) == (self.operation == 'INTERSECTION'):
+ pts = list(reversed(pts))
+
+ pt = wM.inverted() * pts[0]
+
+ # pretranslate
+ o = self.find_in_selection(context, self.auto_update)
+ o.matrix_world = wM * Matrix([
+ [1, 0, 0, pt.x],
+ [0, 1, 0, pt.y],
+ [0, 0, 1, pt.z],
+ [0, 0, 0, 1]
+ ])
+ self.auto_update = False
+ self.from_points(pts)
+ self.auto_update = True
+ self.update_parent(context, o)
+
+ def from_points(self, pts):
+
+ self.n_parts = len(pts) - 2
+
+ self.update_parts()
+
+ p0 = pts.pop(0)
+ a0 = 0
+ for i, p1 in enumerate(pts):
+ dp = p1 - p0
+ da = atan2(dp.y, dp.x) - a0
+ if da > pi:
+ da -= 2 * pi
+ if da < -pi:
+ da += 2 * pi
+ if i >= len(self.parts):
+ # print("Too many pts for parts")
+ break
+ p = self.parts[i]
+ p.length = dp.to_2d().length
+ p.dz = dp.z
+ p.a0 = da
+ a0 += da
+ p0 = p1
+
+ def reverse(self, context, make_ccw=False):
+
+ o = self.find_in_selection(context, self.auto_update)
+
+ g = self.get_generator()
+
+ pts = [seg.p0.to_3d() for seg in g.segs]
+
+ if self.is_cw(pts) != make_ccw:
+ return
+
+ types = [p.type for p in self.parts]
+
+ pts.append(pts[0])
+
+ pts = list(reversed(pts))
+ self.auto_update = False
+
+ self.from_points(pts)
+
+ for i, type in enumerate(reversed(types)):
+ self.parts[i].type = type
+ self.auto_update = True
+ self.update_parent(context, o)
+
+ def update_path(self, context):
+ user_def_path = context.scene.objects.get(self.user_defined_path)
+ if user_def_path is not None and user_def_path.type == 'CURVE':
+ self.from_spline(context,
+ user_def_path.matrix_world,
+ self.user_defined_resolution,
+ user_def_path.data.splines[0])
+
+ def make_surface(self, o, verts, edges):
+ bm = bmesh.new()
+ for v in verts:
+ bm.verts.new(v)
+ bm.verts.ensure_lookup_table()
+ for ed in edges:
+ bm.edges.new((bm.verts[ed[0]], bm.verts[ed[1]]))
+ bm.edges.new((bm.verts[-1], bm.verts[0]))
+ bm.edges.ensure_lookup_table()
+ bm.to_mesh(o.data)
+ bm.free()
+
+ def update(self, context, manipulable_refresh=False, update_parent=False):
+
+ o = self.find_in_selection(context, self.auto_update)
+
+ if o is None:
+ return
+
+ # clean up manipulators before any data model change
+ if manipulable_refresh:
+ self.manipulable_disable(context)
+
+ self.update_parts()
+
+ verts = []
+ edges = []
+
+ g = self.get_generator()
+ g.locate_manipulators()
+
+ # vertex index in order to build axis
+ g.get_verts(verts, edges)
+
+ if len(verts) > 2:
+ self.make_surface(o, verts, edges)
+
+ # enable manipulators rebuild
+ if manipulable_refresh:
+ self.manipulable_refresh = True
+
+ # update parent on direct edit
+ if manipulable_refresh or update_parent:
+ self.update_parent(context, o)
+
+ # restore context
+ self.restore_context(context)
+
+ def manipulable_setup(self, context):
+
+ self.manipulable_disable(context)
+ o = context.active_object
+
+ n_parts = self.n_parts + 1
+
+ self.setup_manipulators()
+
+ for i, part in enumerate(self.parts):
+ if i < n_parts:
+
+ if i > 0:
+ # start angle
+ self.manip_stack.append(part.manipulators[0].setup(context, o, part))
+
+ # length
+ self.manip_stack.append(part.manipulators[1].setup(context, o, part))
+ # index
+ self.manip_stack.append(part.manipulators[3].setup(context, o, self))
+ # offset
+ # self.manip_stack.append(part.manipulators[4].setup(context, o, part))
+
+ # snap point
+ self.manip_stack.append(part.manipulators[2].setup(context, o, self))