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