1# -*- coding:utf-8 -*-
2
3# ##### BEGIN GPL LICENSE BLOCK #####
4#
5#  This program is free software; you can redistribute it and/or
6#  modify it under the terms of the GNU General Public License
7#  as published by the Free Software Foundation; either version 2
8#  of the License, or (at your option) any later version.
9#
10#  This program is distributed in the hope that it will be useful,
11#  but WITHOUT ANY WARRANTY; without even the implied warranty of
12#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13#  GNU General Public License for more details.
14#
15#  You should have received a copy of the GNU General Public License
16#  along with this program; if not, write to the Free Software Foundation,
17#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
18#
19# ##### END GPL LICENSE BLOCK #####
20
21# <pep8 compliant>
22
23# ----------------------------------------------------------
24# Author: Stephen Leger (s-leger)
25#
26# ----------------------------------------------------------
27# noinspection PyUnresolvedReferences
28import bpy
29import time
30# noinspection PyUnresolvedReferences
31from bpy.types import Operator, PropertyGroup, Mesh, Panel
32from bpy.props import (
33    FloatProperty, BoolProperty, IntProperty,
34    StringProperty, EnumProperty,
35    CollectionProperty
36    )
37from .bmesh_utils import BmeshEdit as bmed
38from random import randint
39import bmesh
40from mathutils import Vector, Matrix
41from math import sin, cos, pi, atan2, sqrt, tan
42from .archipack_manipulator import Manipulable, archipack_manipulator
43from .archipack_2d import Line, Arc
44from .archipack_preset import ArchipackPreset, PresetMenuOperator
45from .archipack_object import ArchipackCreateTool, ArchipackObject
46from .archipack_cutter import (
47    CutAblePolygon, CutAbleGenerator,
48    ArchipackCutter,
49    ArchipackCutterPart
50    )
51
52
53class Roof():
54
55    def __init__(self):
56        self.angle_0 = 0
57        self.v0_idx = 0
58        self.v1_idx = 0
59        self.constraint_type = None
60        self.slope_left = 1
61        self.slope_right = 1
62        self.width_left = 1
63        self.width_right = 1
64        self.auto_left = 'AUTO'
65        self.auto_right = 'AUTO'
66        self.type = 'SIDE'
67        # force hip or valley
68        self.enforce_part = 'AUTO'
69        self.triangular_end = False
70        # seg is part of hole
71        self.is_hole = False
72
73    def copy_params(self, s):
74        s.angle_0 = self.angle_0
75        s.v0_idx = self.v0_idx
76        s.v1_idx = self.v1_idx
77        s.constraint_type = self.constraint_type
78        s.slope_left = self.slope_left
79        s.slope_right = self.slope_right
80        s.width_left = self.width_left
81        s.width_right = self.width_right
82        s.auto_left = self.auto_left
83        s.auto_right = self.auto_right
84        s.type = self.type
85        s.enforce_part = self.enforce_part
86        s.triangular_end = self.triangular_end
87        # segment is part of hole / slice
88        s.is_hole = self.is_hole
89
90    @property
91    def copy(self):
92        s = StraightRoof(self.p.copy(), self.v.copy())
93        self.copy_params(s)
94        return s
95
96    def straight(self, length, t=1):
97        s = self.copy
98        s.p = self.lerp(t)
99        s.v = self.v.normalized() * length
100        return s
101
102    def set_offset(self, offset, last=None):
103        """
104            Offset line and compute intersection point
105            between segments
106        """
107        self.line = self.make_offset(offset, last)
108
109    def offset(self, offset):
110        o = self.copy
111        o.p += offset * self.cross_z.normalized()
112        return o
113
114    @property
115    def oposite(self):
116        o = self.copy
117        o.p += o.v
118        o.v = -o.v
119        return o
120
121    @property
122    def t_diff(self):
123        return self.t_end - self.t_start
124
125    def straight_roof(self, a0, length):
126        s = self.straight(length).rotate(a0)
127        r = StraightRoof(s.p, s.v)
128        r.angle_0 = a0
129        return r
130
131    def curved_roof(self, a0, da, radius):
132        n = self.normal(1).rotate(a0).scale(radius)
133        if da < 0:
134            n.v = -n.v
135        c = n.p - n.v
136        r = CurvedRoof(c, radius, n.angle, da)
137        r.angle_0 = a0
138        return r
139
140
141class StraightRoof(Roof, Line):
142    def __str__(self):
143        return "p0:{} p1:{}".format(self.p0, self.p1)
144
145    def __init__(self, p, v):
146        Line.__init__(self, p, v)
147        Roof.__init__(self)
148
149
150class CurvedRoof(Roof, Arc):
151    def __str__(self):
152        return "t_start:{} t_end:{} dist:{}".format(self.t_start, self.t_end, self.dist)
153
154    def __init__(self, c, radius, a0, da):
155        Arc.__init__(self, c, radius, a0, da)
156        Roof.__init__(self)
157
158
159class RoofSegment():
160    """
161        Roof part with 2 polygons
162        and "axis" StraightRoof segment
163    """
164    def __init__(self, seg, left, right):
165        self.seg = seg
166        self.left = left
167        self.right = right
168        self.a0 = 0
169        self.reversed = False
170
171
172class RoofAxisNode():
173    """
174        Connection between parts
175        for radial analysis
176    """
177    def __init__(self):
178        # axis segments
179        self.segs = []
180        self.root = None
181        self.center = 0
182        # store count of horizontal segs
183        self.n_horizontal = 0
184        # store count of slopes segs
185        self.n_slope = 0
186
187    @property
188    def count(self):
189        return len(self.segs)
190
191    @property
192    def last(self):
193        """
194            last segments in this node
195        """
196        return self.segs[-1]
197
198    def left(self, index):
199        if index + 1 >= self.count:
200            return self.segs[0]
201        return self.segs[index + 1]
202
203    def right(self, index):
204        return self.segs[index - 1]
205
206    def add(self, a0, reversed, seg, left, right):
207
208        if seg.constraint_type == 'HORIZONTAL':
209            self.n_horizontal += 1
210        elif seg.constraint_type == 'SLOPE':
211            self.n_slope += 1
212
213        s = RoofSegment(seg, left, right)
214        s.a0 = a0
215        s.reversed = reversed
216        if reversed:
217            self.root = s
218        self.segs.append(s)
219
220    def update_center(self):
221        for i, s in enumerate(self.segs):
222            if s is self.root:
223                self.center = i
224                return
225
226    # sort tree segments by angle
227    def partition(self, array, begin, end):
228        pivot = begin
229        for i in range(begin + 1, end + 1):
230            if array[i].a0 < array[begin].a0:
231                pivot += 1
232                array[i], array[pivot] = array[pivot], array[i]
233        array[pivot], array[begin] = array[begin], array[pivot]
234        return pivot
235
236    def sort(self):
237        def _quicksort(array, begin, end):
238            if begin >= end:
239                return
240            pivot = self.partition(array, begin, end)
241            _quicksort(array, begin, pivot - 1)
242            _quicksort(array, pivot + 1, end)
243
244        end = len(self.segs) - 1
245        _quicksort(self.segs, 0, end)
246
247        # index of root in segs array
248        self.update_center()
249
250
251class RoofPolygon(CutAblePolygon):
252    """
253        ccw roof pitch boundary
254        closed by explicit segment
255        handle triangular shape with zero axis length
256
257        mov  <_________________
258            |                  /\
259            |                  | rot
260            |                  |   left     last <> next
261            \/_____axis_______>|
262
263     node   <_____axis________      next
264            |                  /\
265            |                  | rot
266            |                  |   right    last <> next
267        mov \/________________>|
268                           side angle
269    """
270    def __init__(self, axis, side, fake_axis=None):
271        """
272            Create a default rectangle
273            axis from node to next
274            slope float -z for 1 in side direction
275            side in ['LEFT', 'RIGHT'] in axis direction
276
277            NOTE:
278            when axis length is null (eg: triangular shape)
279            use "fake_axis" with a 1 length to handle
280            distance from segment
281        """
282        if side == 'LEFT':
283            # slope
284            self.slope = axis.slope_left
285            # width
286            self.width = axis.width_left
287            # constraint width
288            self.auto_mode = axis.auto_left
289        else:
290            # slope
291            self.slope = axis.slope_right
292            # width
293            self.width = axis.width_right
294            # constraint width
295            self.auto_mode = axis.auto_right
296
297        self.side = side
298        # backward deps
299        self.backward = False
300        # pointers to neighbors along axis
301        self.last = None
302        self.next = None
303        self.other_side = None
304
305        # axis segment
306        if side == 'RIGHT':
307            self.axis = axis.oposite
308        else:
309            self.axis = axis
310
311        self.fake_axis = None
312
313        # _axis is either a fake one or real one
314        # to prevent further check
315        if fake_axis is None:
316            self._axis = self.axis
317            self.fake_axis = self.axis
318            self.next_cross = axis
319            self.last_cross = axis
320        else:
321            if side == 'RIGHT':
322                self.fake_axis = fake_axis.oposite
323            else:
324                self.fake_axis = fake_axis
325            self._axis = self.fake_axis
326
327        # unit vector perpendicular to axis
328        # looking at outside part
329        v = self.fake_axis.sized_normal(0, -1)
330        self.cross = v
331        self.next_cross = v
332        self.last_cross = v
333
334        self.convex = True
335        # segments from axis end in ccw order
336        # closed by explicit segment
337        self.segs = []
338        # holes
339        self.holes = []
340
341        # Triangular ends
342        self.node_tri = False
343        self.next_tri = False
344        self.is_tri = False
345
346        # sizes
347        self.tmin = 0
348        self.tmax = 1
349        self.dt = 1
350        self.ysize = 0
351        self.xsize = 0
352        self.vx = Vector()
353        self.vy = Vector()
354        self.vz = Vector()
355
356    def move_node(self, p):
357        """
358            Move slope point in node side
359        """
360        if self.side == 'LEFT':
361            self.segs[-1].p0 = p
362            self.segs[2].p1 = p
363        else:
364            self.segs[2].p0 = p
365            self.segs[1].p1 = p
366
367    def move_next(self, p):
368        """
369            Move slope point in next side
370        """
371        if self.side == 'LEFT':
372            self.segs[2].p0 = p
373            self.segs[1].p1 = p
374        else:
375            self.segs[-1].p0 = p
376            self.segs[2].p1 = p
377
378    def node_link(self, da):
379        angle_90 = round(pi / 2, 4)
380        if self.side == 'LEFT':
381            idx = -1
382        else:
383            idx = 1
384        da = abs(round(da, 4))
385        type = "LINK"
386        if da < angle_90:
387            type += "_VALLEY"
388        elif da > angle_90:
389            type += "_HIP"
390        self.segs[idx].type = type
391
392    def next_link(self, da):
393        angle_90 = round(pi / 2, 4)
394        if self.side == 'LEFT':
395            idx = 1
396        else:
397            idx = -1
398        da = abs(round(da, 4))
399        type = "LINK"
400        if da < angle_90:
401            type += "_VALLEY"
402        elif da > angle_90:
403            type += "_HIP"
404        self.segs[idx].type = type
405
406    def bind(self, last, ccw=False):
407        """
408            always in axis real direction
409        """
410        # backward dependency relative to axis
411        if last.backward:
412            self.backward = self.side == last.side
413
414        if self.side == last.side:
415            last.next_cross = self.cross
416        else:
417            last.last_cross = self.cross
418
419        self.last_cross = last.cross
420
421        # axis of last / next segments
422        if self.backward:
423            self.next = last
424            last.last = self
425        else:
426            self.last = last
427            last.next = self
428
429        # width auto
430        if self.auto_mode == 'AUTO':
431            self.width = last.width
432            self.slope = last.slope
433        elif self.auto_mode == 'WIDTH' and self.width != 0:
434            self.slope = last.slope * last.width / self.width
435        elif self.auto_mode == 'SLOPE' and self.slope != 0:
436            self.width = last.width * last.slope / self.slope
437
438        self.make_segments()
439        last.make_segments()
440
441        res, p, t = self.segs[2].intersect(last.segs[2])
442
443        if res:
444            # dont move anything when no intersection found
445            # aka when delta angle == 0
446            self.move_node(p)
447            if self.side != last.side:
448                last.move_node(p)
449            else:
450                last.move_next(p)
451
452        # Free mode
453        # move border
454        # and find intersections
455        # with sides
456        if self.auto_mode == 'ALL':
457            s0 = self._axis.offset(-self.width)
458            res, p0, t = self.segs[1].intersect(s0)
459            if res:
460                self.segs[2].p0 = p0
461                self.segs[1].p1 = p0
462            res, p1, t = self.segs[-1].intersect(s0)
463            if res:
464                self.segs[2].p1 = p1
465                self.segs[-1].p0 = p1
466
467        #   /\
468        #   |   angle
469        #   |____>
470        #
471        # v1 node -> next
472        if self.side == 'LEFT':
473            v1 = self._axis.v
474        else:
475            v1 = -self._axis.v
476
477        if last.side == self.side:
478            # contiguous, v0 node <- next
479
480            # half angle between segments
481            if self.side == 'LEFT':
482                v0 = -last._axis.v
483            else:
484                v0 = last._axis.v
485            da = v0.angle_signed(v1)
486            if ccw:
487                if da < 0:
488                    da = 2 * pi + da
489            elif da > 0:
490                da = da - 2 * pi
491            last.next_link(0.5 * da)
492
493        else:
494            # alternate v0 node -> next
495            # half angle between segments
496            if last.side == 'LEFT':
497                v0 = last._axis.v
498            else:
499                v0 = -last._axis.v
500            da = v0.angle_signed(v1)
501            # angle always ccw
502            if ccw:
503                if da < 0:
504                    da = 2 * pi + da
505            elif da > 0:
506                da = da - 2 * pi
507            last.node_link(0.5 * da)
508
509        self.node_link(-0.5 * da)
510
511    def next_seg(self, index):
512        idx = self.get_index(index + 1)
513        return self.segs[idx]
514
515    def last_seg(self, index):
516        return self.segs[index - 1]
517
518    def make_segments(self):
519        if len(self.segs) < 1:
520            s0 = self._axis
521            w = self.width
522            s1 = s0.straight(w, 1).rotate(pi / 2)
523            s1.type = 'SIDE'
524            s3 = s0.straight(w, 0).rotate(pi / 2).oposite
525            s3.type = 'SIDE'
526            s2 = StraightRoof(s1.p1, s3.p0 - s1.p1)
527            s2.type = 'BOTTOM'
528            self.segs = [s0, s1, s2, s3]
529
530    def move_side(self, pt):
531        """
532            offset side to point
533        """
534        s2 = self.segs[2]
535        d0, t = self.distance(s2.p0)
536        d1, t = self.distance(pt)
537        # adjust width and slope according
538        self.width = d1
539        self.slope = self.slope * d0 / d1
540        self.segs[2] = s2.offset(d1 - d0)
541
542    def propagate_backward(self, pt):
543        """
544            Propagate slope, keep 2d angle of slope
545            Move first point and border
546            keep border parallel
547            adjust slope
548            and next shape
549        """
550        # distance of p
551        # offset side to point
552        self.move_side(pt)
553
554        # move verts on node side
555        self.move_next(pt)
556
557        if self.side == 'LEFT':
558            # move verts on next side
559            res, p, t = self.segs[-1].intersect(self.segs[2])
560        else:
561            # move verts on next side
562            res, p, t = self.segs[1].intersect(self.segs[2])
563
564        if res:
565            self.move_node(p)
566
567            if self.next is not None and self.next.auto_mode in {'AUTO'}:
568                self.next.propagate_backward(p)
569
570    def propagate_forward(self, pt):
571        """
572            Propagate slope, keep 2d angle of slope
573            Move first point and border
574            keep border parallel
575            adjust slope
576            and next shape
577        """
578        # offset side to point
579        self.move_side(pt)
580
581        # move verts on node side
582        self.move_node(pt)
583        if self.side == 'LEFT':
584            # move verts on next side
585            res, p, t = self.segs[1].intersect(self.segs[2])
586        else:
587            # move verts on next side
588            res, p, t = self.segs[-1].intersect(self.segs[2])
589
590        if res:
591            self.move_next(p)
592            if self.next is not None and self.next.auto_mode in {'AUTO'}:
593                self.next.propagate_forward(p)
594
595    def rotate_next_slope(self, a0):
596        """
597            Rotate next slope part
598        """
599        if self.side == 'LEFT':
600            s0 = self.segs[1].rotate(a0)
601            s1 = self.segs[2]
602            res, p, t = s1.intersect(s0)
603        else:
604            s0 = self.segs[2]
605            s1 = self.segs[-1]
606            res, p, t = s1.oposite.rotate(-a0).intersect(s0)
607
608        if res:
609            s1.p0 = p
610            s0.p1 = p
611
612            if self.next is not None:
613                if self.next.auto_mode == 'ALL':
614                    return
615                if self.next.backward:
616                    self.next.propagate_backward(p)
617                else:
618                    self.next.propagate_forward(p)
619
620    def rotate_node_slope(self, a0):
621        """
622            Rotate node slope part
623        """
624        if self.side == 'LEFT':
625            s0 = self.segs[2]
626            s1 = self.segs[-1]
627            res, p, t = s1.oposite.rotate(-a0).intersect(s0)
628        else:
629            s0 = self.segs[1].rotate(a0)
630            s1 = self.segs[2]
631            res, p, t = s1.intersect(s0)
632
633        if res:
634            s1.p0 = p
635            s0.p1 = p
636
637            if self.next is not None:
638                if self.next.auto_mode == 'ALL':
639                    return
640                if self.next.backward:
641                    self.next.propagate_backward(p)
642                else:
643                    self.next.propagate_forward(p)
644
645    def distance(self, pt):
646        """
647            distance from axis
648            always use fake_axis here to
649            allow axis being cut and
650            still work
651        """
652        res, d, t = self.fake_axis.point_sur_segment(pt)
653        return d, t
654
655    def altitude(self, pt):
656        d, t = self.distance(pt)
657        return -d * self.slope
658
659    def uv(self, pt):
660        d, t = self.distance(pt)
661        return ((t - self.tmin) * self.xsize, d)
662
663    def intersect(self, seg):
664        """
665            compute intersections of a segment with boundaries
666            segment must start on axis
667            return segments inside
668        """
669        it = []
670        for s in self.segs:
671            res, p, t, u = seg.intersect_ext(s)
672            if res:
673                it.append((t, p))
674        return it
675
676    def merge(self, other):
677
678        raise NotImplementedError
679
680    def draw(self, context, z, verts, edges):
681        f = len(verts)
682        #
683        #   0_______1
684        #   |_______|
685        #   3       2
686        verts.extend([(s.p0.x, s.p0.y, z + self.altitude(s.p0)) for s in self.segs])
687        n_segs = len(self.segs) - 1
688        edges.extend([[f + i, f + i + 1] for i in range(n_segs)])
689        edges.append([f + n_segs, f])
690        """
691        f = len(verts)
692        verts.extend([(s.p1.x, s.p1.y, z + self.altitude(s.p1)) for s in self.segs])
693        n_segs = len(self.segs) - 1
694        edges.extend([[f + i, f + i + 1] for i in range(n_segs)])
695        edges.append([f + n_segs, f])
696        """
697        # holes
698        for hole in self.holes:
699            f = len(verts)
700            #
701            #   0_______1
702            #   |_______|
703            #   3       2
704            verts.extend([(s.p0.x, s.p0.y, z + self.altitude(s.p0)) for s in hole.segs])
705            n_segs = len(hole.segs) - 1
706            edges.extend([[f + i, f + i + 1] for i in range(n_segs)])
707            edges.append([f + n_segs, f])
708
709        # axis
710        """
711        f = len(verts)
712        verts.extend([self.axis.p0.to_3d(), self.axis.p1.to_3d()])
713        edges.append([f, f + 1])
714
715        # cross
716        f = len(verts)
717        verts.extend([self.axis.lerp(0.5).to_3d(), (self.axis.lerp(0.5) + self.cross.v).to_3d()])
718        edges.append([f, f + 1])
719        """
720
721        # relationships arrows
722        if self.next or self.last:
723            w = 0.2
724            s0 = self._axis.offset(-0.5 * self.ysize)
725            p0 = s0.lerp(0.4).to_3d()
726            p0.z = z
727            p1 = s0.lerp(0.6).to_3d()
728            p1.z = z
729            if self.side == 'RIGHT':
730                p0, p1 = p1, p0
731            if self.backward:
732                p0, p1 = p1, p0
733            s1 = s0.sized_normal(0.5, w)
734            s2 = s0.sized_normal(0.5, -w)
735            f = len(verts)
736            p2 = s1.p1.to_3d()
737            p2.z = z
738            p3 = s2.p1.to_3d()
739            p3.z = z
740            verts.extend([p1, p0, p2, p3])
741            edges.extend([[f + 1, f], [f + 2, f], [f + 3, f]])
742
743    def as_string(self):
744        """
745            Print strips relationships
746        """
747        if self.backward:
748            dir = "/\\"
749            print("%s next" % (dir))
750        else:
751            dir = "\\/"
752            print("%s node" % (dir))
753        print("%s %s" % (dir, self.side))
754        if self.backward:
755            print("%s node" % (dir))
756        else:
757            print("%s next" % (dir))
758        if self.next:
759            print("_________")
760            self.next.as_string()
761        else:
762            print("#########")
763
764    def limits(self):
765        dist = []
766        param_t = []
767        for s in self.segs:
768            res, d, t = self.fake_axis.point_sur_segment(s.p0)
769            param_t.append(t)
770            dist.append(d)
771
772        if len(param_t) > 0:
773            self.tmin = min(param_t)
774            self.tmax = max(param_t)
775        else:
776            self.tmin = 0
777            self.tmax = 1
778
779        self.dt = self.tmax - self.tmin
780
781        if len(dist) > 0:
782            self.ysize = max(dist)
783        else:
784            self.ysize = 0
785
786        self.xsize = self.fake_axis.length * self.dt
787        # vectors components of part matrix
788        # where x is is axis direction
789        # y down
790        # z up
791        vx = -self.fake_axis.v.normalized().to_3d()
792        vy = Vector((-vx.y, vx.x, self.slope)).normalized()
793        self.vx = vx
794        self.vy = vy
795        self.vz = vx.cross(vy)
796
797
798"""
799import bmesh
800m = C.object.data
801[(round(v.co.x, 3), round(v.co.y, 3), round(v.co.z, 3)) for v in m.vertices]
802[tuple(p.vertices) for p in m.polygons]
803
804uvs = []
805bpy.ops.object.mode_set(mode='EDIT')
806bm = bmesh.from_edit_mesh(m)
807[tuple(i.index for i in edge.verts) for edge in bm.edges]
808
809layer = bm.loops.layers.uv.verify()
810for i, face in enumerate(bm.faces):
811    uv = []
812    for j, loop in enumerate(face.loops):
813        co = loop[layer].uv
814        uv.append((round(co.x, 2), round(co.y, 2)))
815    uvs.append(uv)
816uvs
817"""
818
819
820class RoofGenerator(CutAbleGenerator):
821
822    def __init__(self, d, origin=Vector((0, 0, 0))):
823        self.parts = d.parts
824        self.segs = []
825        self.nodes = []
826        self.pans = []
827        self.length = 0
828        self.origin = origin.to_2d()
829        self.z = origin.z
830        self.width_right = d.width_right
831        self.width_left = d.width_left
832        self.slope_left = d.slope_left
833        self.slope_right = d.slope_right
834        self.user_defined_tile = None
835        self.user_defined_uvs = None
836        self.user_defined_mat = None
837        self.is_t_child = d.t_parent != ""
838
839    def add_part(self, part):
840
841        if len(self.segs) < 1 or part.bound_idx < 1:
842            s = None
843        else:
844            s = self.segs[part.bound_idx - 1]
845
846        a0 = part.a0
847
848        if part.constraint_type == 'SLOPE' and a0 == 0:
849            a0 = 90
850
851        # start a new roof
852        if s is None:
853            v = part.length * Vector((cos(a0), sin(a0)))
854            s = StraightRoof(self.origin, v)
855        else:
856            s = s.straight_roof(a0, part.length)
857
858        # parent segment (root) index is  v0_idx - 1
859        s.v0_idx = min(len(self.segs), part.bound_idx)
860
861        s.constraint_type = part.constraint_type
862
863        if part.constraint_type == 'SLOPE':
864            s.enforce_part = part.enforce_part
865        else:
866            s.enforce_part = 'AUTO'
867
868        s.angle_0 = a0
869        s.take_precedence = part.take_precedence
870        s.auto_right = part.auto_right
871        s.auto_left = part.auto_left
872        s.width_left = part.width_left
873        s.width_right = part.width_right
874        s.slope_left = part.slope_left
875        s.slope_right = part.slope_right
876        s.type = 'AXIS'
877        s.triangular_end = part.triangular_end
878        self.segs.append(s)
879
880    def locate_manipulators(self):
881        """
882
883        """
884        for i, f in enumerate(self.segs):
885
886            manipulators = self.parts[i].manipulators
887            p0 = f.p0.to_3d()
888            p0.z = self.z
889            p1 = f.p1.to_3d()
890            p1.z = self.z
891            # angle from last to current segment
892            if i > 0:
893
894                manipulators[0].type_key = 'ANGLE'
895                v0 = self.segs[f.v0_idx - 1].straight(-1, 1).v.to_3d()
896                v1 = f.straight(1, 0).v.to_3d()
897                manipulators[0].set_pts([p0, v0, v1])
898
899            # segment length
900            manipulators[1].type_key = 'SIZE'
901            manipulators[1].prop1_name = "length"
902            manipulators[1].set_pts([p0, p1, (1.0, 0, 0)])
903
904            # dumb segment id
905            manipulators[2].set_pts([p0, p1, (1, 0, 0)])
906
907            p0 = f.lerp(0.5).to_3d()
908            p0.z = self.z
909            # size left
910            p1 = f.sized_normal(0.5, -self.parts[i].width_left).p1.to_3d()
911            p1.z = self.z
912            manipulators[3].set_pts([p0, p1, (1, 0, 0)])
913
914            # size right
915            p1 = f.sized_normal(0.5, self.parts[i].width_right).p1.to_3d()
916            p1.z = self.z
917            manipulators[4].set_pts([p0, p1, (-1, 0, 0)])
918
919            # slope left
920            n0 = f.sized_normal(0.5, -1)
921            p0 = n0.p1.to_3d()
922            p0.z = self.z
923            p1 = p0.copy()
924            p1.z = self.z - self.parts[i].slope_left
925            manipulators[5].set_pts([p0, p1, (-1, 0, 0)], normal=n0.v.to_3d())
926
927            # slope right
928            n0 = f.sized_normal(0.5, 1)
929            p0 = n0.p1.to_3d()
930            p0.z = self.z
931            p1 = p0.copy()
932            p1.z = self.z - self.parts[i].slope_right
933            manipulators[6].set_pts([p0, p1, (1, 0, 0)], normal=n0.v.to_3d())
934
935    def seg_partition(self, array, begin, end):
936        """
937            sort tree segments by angle
938        """
939        pivot = begin
940        for i in range(begin + 1, end + 1):
941            if array[i].a0 < array[begin].a0:
942                pivot += 1
943                array[i], array[pivot] = array[pivot], array[i]
944        array[pivot], array[begin] = array[begin], array[pivot]
945        return pivot
946
947    def sort_seg(self, array, begin=0, end=None):
948        # print("sort_child")
949        if end is None:
950            end = len(array) - 1
951
952        def _quicksort(array, begin, end):
953            if begin >= end:
954                return
955            pivot = self.seg_partition(array, begin, end)
956            _quicksort(array, begin, pivot - 1)
957            _quicksort(array, pivot + 1, end)
958        return _quicksort(array, begin, end)
959
960    def make_roof(self, context):
961        """
962            Init data structure for possibly multi branched nodes
963            nodes : radial relationships
964            pans : quad strip linear relationships
965        """
966
967        pans = []
968
969        # node are connected segments
970        # node
971        # (segment idx)
972        # (angle from root part > 0 right)
973        # (reversed) a seg connected by p1
974        #            "root" of node
975        nodes = [RoofAxisNode() for s in range(len(self.segs) + 1)]
976
977        # Init width on seg 0
978        s0 = self.segs[0]
979        if self.parts[0].auto_left in {'AUTO', 'SLOPE'}:
980            s0.width_left = self.width_left
981        if self.parts[0].auto_right in {'AUTO', 'SLOPE'}:
982            s0.width_right = self.width_right
983        if self.parts[0].auto_left in {'AUTO', 'WIDTH'}:
984            s0.slope_left = self.slope_left
985        if self.parts[0].auto_left in {'AUTO', 'WIDTH'}:
986            s0.slope_right = self.slope_right
987
988        # make nodes with HORIZONTAL constraints
989        for idx, s in enumerate(self.segs):
990            s.v1_idx = idx + 1
991            if s.constraint_type == 'HORIZONTAL':
992                left = RoofPolygon(s, 'LEFT')
993                right = RoofPolygon(s, 'RIGHT')
994                left.other_side = right
995                right.other_side = left
996                rs = RoofSegment(s, left, right)
997                pans.append(rs)
998                nodes[s.v0_idx].add(s.angle_0, False, s, left, right)
999                nodes[s.v1_idx].add(-pi, True, s, left, right)
1000
1001        # set first node root
1002        # so regular sort does work
1003        nodes[0].root = nodes[0].segs[0]
1004        self.nodes = nodes
1005        # Propagate slope and width
1006        # on node basis along axis
1007        # bi-direction Radial around node
1008        # from left and right to center
1009        # contiguous -> same
1010        # T: and (x % 2 == 1)
1011        # First one take precedence over others
1012        # others inherit from side
1013        #
1014        #         l / rb    l = left
1015        #          3        r = right
1016        #   l _1_ /         b = backward
1017        #   r     \
1018        #          2
1019        #          r\ l
1020        #
1021        # X: right one or left one l (x % 2 == 0)
1022        # inherits from side
1023        #
1024        #    l 3 lb         l = left
1025        # l__1_|_2_l        r = right
1026        # r    |   r        b = backward -> propagate in reverse axis direction
1027        #    r 4 rb
1028        #
1029        # for idx, node in enumerate(nodes):
1030        #    print("idx:%s node:%s" % (idx, node.root))
1031
1032        for idx, node in enumerate(nodes):
1033
1034            node.sort()
1035
1036            nb_segs = node.count
1037
1038            if node.root is None:
1039                continue
1040
1041            left = node.root.left
1042            right = node.root.right
1043
1044            # basic one single node
1045            if nb_segs < 2:
1046                left.make_segments()
1047                right.make_segments()
1048                continue
1049
1050            # get "root" slope and width
1051            l_bind = left
1052            r_bind = right
1053
1054            # simple case: 2 contiguous segments
1055            if nb_segs == 2:
1056                s = node.last
1057                s.right.bind(r_bind, ccw=False)
1058                s.left.bind(l_bind, ccw=True)
1059                continue
1060
1061            # More than 2 segments, uneven distribution
1062            if nb_segs % 2 == 1:
1063                # find which child does take precedence
1064                # first one on rootline (arbitrary)
1065                center = (nb_segs - 1) / 2
1066            else:
1067                # even distribution
1068                center = nb_segs / 2
1069
1070            # user defined precedence if any
1071            for i, s in enumerate(node.segs):
1072                if s.seg.take_precedence:
1073                    center = i
1074                    break
1075
1076            # bind right side to center
1077            for i, s in enumerate(node.segs):
1078                # skip axis
1079                if i > 0:
1080                    if i < center:
1081                        # right contiguous with last
1082                        s.right.bind(r_bind, ccw=False)
1083
1084                        # next bind to left
1085                        r_bind = s.left
1086
1087                        # left backward, not bound
1088                        # so setup width and slope
1089                        if s.left.auto_mode in {'AUTO', 'WIDTH'}:
1090                            s.left.slope = right.slope
1091                        if s.left.auto_mode in {'AUTO', 'SLOPE'}:
1092                            s.left.width = right.width
1093                        s.left.backward = True
1094                    else:
1095                        # right bound to last
1096                        s.right.bind(r_bind, ccw=False)
1097                        break
1098
1099            # bind left side to center
1100            for i, s in enumerate(reversed(node.segs)):
1101                # skip axis
1102                if i < nb_segs - center - 1:
1103                    # left contiguous with last
1104                    s.left.bind(l_bind, ccw=True)
1105                    # next bind to right
1106                    l_bind = s.right
1107                    # right backward, not bound
1108                    # so setup width and slope
1109                    if s.right.auto_mode in {'AUTO', 'WIDTH'}:
1110                        s.right.slope = left.slope
1111                    if s.right.auto_mode in {'AUTO', 'SLOPE'}:
1112                        s.right.width = left.width
1113                    s.right.backward = True
1114                else:
1115                    # right bound to last
1116                    s.left.bind(l_bind, ccw=True)
1117                    break
1118
1119        # slope constraints allowed between segments
1120        # multiple (up to 2) on start and end
1121        # single between others
1122        #
1123        #    2 slope            2 slope           2 slope
1124        #     |                  |                 |
1125        #     |______section_1___|___section_2_____|
1126        #     |                  |                 |
1127        #     |                  |                 |
1128        #    multiple           single            multiple
1129
1130        # add slopes constraints to nodes
1131        for i, s in enumerate(self.segs):
1132            if s.constraint_type == 'SLOPE':
1133                nodes[s.v0_idx].add(s.angle_0, False, s, None, None)
1134
1135        # sort nodes, remove duplicate slopes between
1136        # horizontal, keeping only first one
1137        for idx, node in enumerate(nodes):
1138            to_remove = []
1139            node.sort()
1140            # remove dup between all
1141            # but start / end nodes
1142            if node.n_horizontal > 1:
1143                last = None
1144                for i, s in enumerate(node.segs):
1145                    if s.seg.constraint_type == last:
1146                        if s.seg.constraint_type == 'SLOPE':
1147                            to_remove.append(i)
1148                    last = s.seg.constraint_type
1149                for i in reversed(to_remove):
1150                    node.segs.pop(i)
1151                node.update_center()
1152
1153        for idx, node in enumerate(nodes):
1154
1155            # a node may contain many slopes
1156            # 2 * (part starting from node - 1)
1157            #
1158            #        s0
1159            # root 0 |_______
1160            #        |
1161            #        s1
1162            #
1163            #               s1
1164            # root   _______|
1165            #               |
1166            #               s0
1167            #
1168            #       s3  3  s2
1169            #     l   \l|r/ l
1170            # root  ___\|/___ 2
1171            #     r    /|\  r
1172            #         /r|l\
1173            #       s0  1  s1
1174            #
1175            #        s2  s1=slope
1176            #        |r /
1177            #        | / l
1178            #        |/____s
1179            #
1180            # root to first child -> equal side
1181            # any other childs -> oposite sides
1182
1183            if node.n_horizontal == 1:
1184                # slopes at start or end of segment
1185                # segment slope is not affected
1186                if node.n_slope > 0:
1187                    # node has user def slope
1188                    s = node.root
1189                    s0 = node.left(node.center)
1190                    a0 = s0.seg.delta_angle(s.seg)
1191                    if node.root.reversed:
1192                        # slope at end of segment
1193                        # first one is right or left
1194                        if a0 < 0:
1195                            # right side
1196                            res, p, t = s0.seg.intersect(s.right.segs[2])
1197                            s.right.segs[-1].p0 = p
1198                            s.right.segs[2].p1 = p
1199                        else:
1200                            # left side
1201                            res, p, t = s0.seg.intersect(s.left.segs[2])
1202                            s.left.segs[1].p1 = p
1203                            s.left.segs[2].p0 = p
1204                        if node.n_slope > 1:
1205                            # last one must be left
1206                            s1 = node.right(node.center)
1207                            a1 = s1.seg.delta_angle(s.seg)
1208                            # both slopes on same side:
1209                            # skip this one
1210                            if a0 > 0 and a1 < 0:
1211                                # right side
1212                                res, p, t = s1.seg.intersect(s.right.segs[2])
1213                                s.right.segs[-1].p0 = p
1214                                s.right.segs[2].p1 = p
1215                            if a0 < 0 and a1 > 0:
1216                                # left side
1217                                res, p, t = s1.seg.intersect(s.left.segs[2])
1218                                s.left.segs[1].p1 = p
1219                                s.left.segs[2].p0 = p
1220
1221                    else:
1222                        # slope at start of segment
1223                        if a0 < 0:
1224                            # right side
1225                            res, p, t = s0.seg.intersect(s.right.segs[2])
1226                            s.right.segs[1].p1 = p
1227                            s.right.segs[2].p0 = p
1228                        else:
1229                            # left side
1230                            res, p, t = s0.seg.intersect(s.left.segs[2])
1231                            s.left.segs[-1].p0 = p
1232                            s.left.segs[2].p1 = p
1233                        if node.n_slope > 1:
1234                            # last one must be right
1235                            s1 = node.right(node.center)
1236                            a1 = s1.seg.delta_angle(s.seg)
1237                            # both slopes on same side:
1238                            # skip this one
1239                            if a0 > 0 and a1 < 0:
1240                                # right side
1241                                res, p, t = s1.seg.intersect(s.right.segs[2])
1242                                s.right.segs[1].p1 = p
1243                                s.right.segs[2].p0 = p
1244                            if a0 < 0 and a1 > 0:
1245                                # left side
1246                                res, p, t = s1.seg.intersect(s.left.segs[2])
1247                                s.left.segs[-1].p0 = p
1248                                s.left.segs[2].p1 = p
1249
1250            else:
1251                # slopes between segments
1252                # does change next segment slope
1253                for i, s0 in enumerate(node.segs):
1254                    s1 = node.left(i)
1255                    s2 = node.left(i + 1)
1256
1257                    if s1.seg.constraint_type == 'SLOPE':
1258
1259                        # 3 cases:
1260                        # s0 is root contiguous -> sides are same
1261                        # s2 is root contiguous -> sides are same
1262                        # back to back -> sides are not same
1263
1264                        if s0.reversed:
1265                            # contiguous right / right
1266                            # 2 cases
1267                            # right is backward
1268                            # right is forward
1269                            if s2.right.backward:
1270                                # s0 depends on s2
1271                                main = s2.right
1272                                v = main.segs[1].v
1273                            else:
1274                                # s2 depends on s0
1275                                main = s0.right
1276                                v = -main.segs[-1].v
1277                            res, p, t = s1.seg.intersect(main.segs[2])
1278                            if res:
1279                                # slope vector
1280                                dp = p - s1.seg.p0
1281                                a0 = dp.angle_signed(v)
1282                                if s2.right.backward:
1283                                    main.rotate_node_slope(a0)
1284                                else:
1285                                    main.rotate_next_slope(-a0)
1286                        elif s2.reversed:
1287                            # contiguous left / left
1288                            # 2 cases
1289                            # left is backward
1290                            # left is forward
1291                            if s0.left.backward:
1292                                # s0 depends on s2
1293                                main = s0.left
1294                                v = -main.segs[-1].v
1295                            else:
1296                                # s2 depends on s0
1297                                main = s2.left
1298                                v = main.segs[1].v
1299                            res, p, t = s1.seg.intersect(main.segs[2])
1300                            if res:
1301                                # slope vector
1302                                dp = p - s1.seg.p0
1303                                a0 = dp.angle_signed(v)
1304                                if s0.left.backward:
1305                                    main.rotate_node_slope(-a0)
1306                                else:
1307                                    main.rotate_next_slope(a0)
1308                        else:
1309                            # back left / right
1310                            # 2 cases
1311                            # left is backward
1312                            # left is forward
1313                            if s0.left.backward:
1314                                # s2 depends on s0
1315                                main = s0.left
1316                                v = -main.segs[-1].v
1317                            else:
1318                                # s0 depends on s2
1319                                main = s2.right
1320                                v = main.segs[1].v
1321
1322                            res, p, t = s1.seg.intersect(main.segs[2])
1323                            if res:
1324                                # slope vector
1325                                dp = p - s1.seg.p0
1326                                a0 = dp.angle_signed(v)
1327                                if s0.left.backward:
1328                                    main.rotate_node_slope(-a0)
1329                                else:
1330                                    main.rotate_node_slope(a0)
1331
1332        self.pans = []
1333
1334        # triangular ends
1335        for node in self.nodes:
1336            if node.root is None:
1337                continue
1338            if node.n_horizontal == 1 and node.root.seg.triangular_end:
1339                if node.root.reversed:
1340                    # Next side (segment end)
1341                    left = node.root.left
1342                    right = node.root.right
1343                    left.next_tri = True
1344                    right.next_tri = True
1345
1346                    s0 = left.segs[1]
1347                    s1 = left.segs[2]
1348                    s2 = right.segs[-1]
1349                    s3 = right.segs[2]
1350                    p0 = s1.lerp(-left.width / s1.length)
1351                    p1 = s0.p0
1352                    p2 = s3.lerp(1 + right.width / s3.length)
1353
1354                    # compute slope from points
1355                    p3 = p0.to_3d()
1356                    p3.z = -left.width * left.slope
1357                    p4 = p1.to_3d()
1358                    p5 = p2.to_3d()
1359                    p5.z = -right.width * right.slope
1360                    n = (p3 - p4).normalized().cross((p5 - p4).normalized())
1361                    v = n.cross(Vector((0, 0, 1)))
1362                    dz = n.cross(v)
1363
1364                    # compute axis
1365                    s = StraightRoof(p1, v)
1366                    res, d0, t = s.point_sur_segment(p0)
1367                    res, d1, t = s.point_sur_segment(p2)
1368                    p = RoofPolygon(s, 'RIGHT')
1369                    p.make_segments()
1370                    p.slope = -dz.z / dz.to_2d().length
1371                    p.is_tri = True
1372
1373                    p.cross = StraightRoof(p1, (p2 - p0)).sized_normal(0, -1)
1374                    p.next_cross = left.cross
1375                    p.last_cross = right.cross
1376                    right.next_cross = p.cross
1377                    left.next_cross = p.cross
1378
1379                    # remove axis seg of tri
1380                    p.segs[-1].p0 = p0
1381                    p.segs[-1].p1 = p1
1382                    p.segs[2].p0 = p2
1383                    p.segs[2].p1 = p0
1384                    p.segs[1].p1 = p2
1385                    p.segs[1].p0 = p1
1386                    p.segs[1].type = 'LINK_HIP'
1387                    p.segs[-1].type = 'LINK_HIP'
1388                    p.segs.pop(0)
1389                    # adjust left and side borders
1390                    s0.p1 = p0
1391                    s1.p0 = p0
1392                    s2.p0 = p2
1393                    s3.p1 = p2
1394                    s0.type = 'LINK_HIP'
1395                    s2.type = 'LINK_HIP'
1396                    self.pans.append(p)
1397
1398                elif not self.is_t_child:
1399                    # no triangular part with t_child
1400                    # on "node" parent roof side
1401                    left = node.root.left
1402                    right = node.root.right
1403                    left.node_tri = True
1404                    right.node_tri = True
1405                    s0 = right.segs[1]
1406                    s1 = right.segs[2]
1407                    s2 = left.segs[-1]
1408                    s3 = left.segs[2]
1409                    p0 = s1.lerp(-right.width / s1.length)
1410                    p1 = s0.p0
1411                    p2 = s3.lerp(1 + left.width / s3.length)
1412
1413                    # compute axis and slope from points
1414                    p3 = p0.to_3d()
1415                    p3.z = -right.width * right.slope
1416                    p4 = p1.to_3d()
1417                    p5 = p2.to_3d()
1418                    p5.z = -left.width * left.slope
1419                    n = (p3 - p4).normalized().cross((p5 - p4).normalized())
1420                    v = n.cross(Vector((0, 0, 1)))
1421                    dz = n.cross(v)
1422
1423                    s = StraightRoof(p1, v)
1424                    p = RoofPolygon(s, 'RIGHT')
1425                    p.make_segments()
1426                    p.slope = -dz.z / dz.to_2d().length
1427                    p.is_tri = True
1428
1429                    p.cross = StraightRoof(p1, (p2 - p0)).sized_normal(0, -1)
1430                    p.next_cross = right.cross
1431                    p.last_cross = left.cross
1432                    right.last_cross = p.cross
1433                    left.last_cross = p.cross
1434
1435                    # remove axis seg of tri
1436                    p.segs[-1].p0 = p0
1437                    p.segs[-1].p1 = p1
1438                    p.segs[2].p0 = p2
1439                    p.segs[2].p1 = p0
1440                    p.segs[1].p1 = p2
1441                    p.segs[1].p0 = p1
1442                    p.segs[1].type = 'LINK_HIP'
1443                    p.segs[-1].type = 'LINK_HIP'
1444                    p.segs.pop(0)
1445                    # adjust left and side borders
1446                    s0.p1 = p0
1447                    s1.p0 = p0
1448                    s2.p0 = p2
1449                    s3.p1 = p2
1450                    s0.type = 'LINK_HIP'
1451                    s2.type = 'LINK_HIP'
1452                    self.pans.append(p)
1453
1454        # make flat array
1455        for pan in pans:
1456            self.pans.extend([pan.left, pan.right])
1457
1458        # merge contiguous with 0 angle diff
1459        to_remove = []
1460        for i, pan in enumerate(self.pans):
1461            if pan.backward:
1462                next = pan.last
1463                if next is not None:
1464                    # same side only can merge
1465                    if next.side == pan.side:
1466                        if round(next._axis.delta_angle(pan._axis), 4) == 0:
1467                            to_remove.append(i)
1468                            next.next = pan.next
1469                            next.last_cross = pan.last_cross
1470                            next.node_tri = pan.node_tri
1471
1472                            next.slope = pan.slope
1473                            if pan.side == 'RIGHT':
1474                                if next.backward:
1475                                    next._axis.p1 = pan._axis.p1
1476                                    next.segs[1] = pan.segs[1]
1477                                    next.segs[2].p0 = pan.segs[2].p0
1478                                else:
1479                                    next._axis.p0 = pan._axis.p0
1480                                    next.segs[-1] = pan.segs[-1]
1481                                    next.segs[2].p1 = pan.segs[2].p1
1482                            else:
1483                                if next.backward:
1484                                    next._axis.p0 = pan._axis.p0
1485                                    next.segs[-1] = pan.segs[-1]
1486                                    next.segs[2].p1 = pan.segs[2].p1
1487                                else:
1488                                    next._axis.p1 = pan._axis.p1
1489                                    next.segs[1] = pan.segs[1]
1490                                    next.segs[2].p0 = pan.segs[2].p0
1491            else:
1492                next = pan.next
1493                if next is not None:
1494                    # same side only can merge
1495                    if next.side == pan.side:
1496                        if round(next._axis.delta_angle(pan._axis), 4) == 0:
1497                            to_remove.append(i)
1498                            next.last = pan.last
1499                            next.last_cross = pan.last_cross
1500                            next.node_tri = pan.node_tri
1501
1502                            next.slope = pan.slope
1503                            if pan.side == 'LEFT':
1504                                if next.backward:
1505                                    next._axis.p1 = pan._axis.p1
1506                                    next.segs[1] = pan.segs[1]
1507                                    next.segs[2].p0 = pan.segs[2].p0
1508                                else:
1509                                    next._axis.p0 = pan._axis.p0
1510                                    next.segs[-1] = pan.segs[-1]
1511                                    next.segs[2].p1 = pan.segs[2].p1
1512                            else:
1513                                if next.backward:
1514                                    next._axis.p0 = pan._axis.p0
1515                                    next.segs[-1] = pan.segs[-1]
1516                                    next.segs[2].p1 = pan.segs[2].p1
1517                                else:
1518                                    next._axis.p1 = pan._axis.p1
1519                                    next.segs[1] = pan.segs[1]
1520                                    next.segs[2].p0 = pan.segs[2].p0
1521
1522        for i in reversed(to_remove):
1523            self.pans.pop(i)
1524
1525        # compute limits
1526        for pan in self.pans:
1527            pan.limits()
1528
1529        """
1530        for pan in self.pans:
1531            if pan.last is None:
1532                pan.as_string()
1533        """
1534        return
1535
1536    def lambris(self, context, o, d):
1537
1538        idmat = 0
1539        lambris_height = 0.02
1540        alt = self.z - lambris_height
1541        for pan in self.pans:
1542
1543            verts = []
1544            faces = []
1545            matids = []
1546            uvs = []
1547
1548            f = len(verts)
1549            verts.extend([(s.p0.x, s.p0.y, alt + pan.altitude(s.p0)) for s in pan.segs])
1550            uvs.append([pan.uv(s.p0) for s in pan.segs])
1551            n_segs = len(pan.segs)
1552            face = [f + i for i in range(n_segs)]
1553            faces.append(face)
1554            matids.append(idmat)
1555
1556            bm = bmed.buildmesh(
1557                context, o, verts, faces, matids=matids, uvs=uvs,
1558                weld=False, clean=False, auto_smooth=True, temporary=True)
1559
1560            self.cut_holes(bm, pan)
1561
1562            bmesh.ops.dissolve_limit(bm,
1563                        angle_limit=0.01,
1564                        use_dissolve_boundaries=False,
1565                        verts=bm.verts,
1566                        edges=bm.edges,
1567                        delimit={'MATERIAL'})
1568
1569            geom = bm.faces[:]
1570            verts = bm.verts[:]
1571            bmesh.ops.solidify(bm, geom=geom, thickness=0.0001)
1572            bmesh.ops.translate(bm, vec=Vector((0, 0, lambris_height)), space=o.matrix_world, verts=verts)
1573
1574            # merge with object
1575            bmed.bmesh_join(context, o, [bm], normal_update=True)
1576
1577        bpy.ops.object.mode_set(mode='OBJECT')
1578
1579    def couverture(self, context, o, d):
1580
1581        idmat = 7
1582        rand = 3
1583        ttl = len(self.pans)
1584        if ttl < 1:
1585            return
1586
1587        sx, sy, sz = d.tile_size_x, d.tile_size_y, d.tile_size_z
1588
1589        """
1590        /* Bevel offset_type slot values */
1591        enum {
1592          BEVEL_AMT_OFFSET,
1593          BEVEL_AMT_WIDTH,
1594          BEVEL_AMT_DEPTH,
1595          BEVEL_AMT_PERCENT
1596        };
1597        """
1598        offset_type = 'PERCENT'
1599
1600        if d.tile_offset > 0:
1601            offset = - d.tile_offset / 100
1602        else:
1603            offset = 0
1604
1605        if d.tile_model == 'BRAAS2':
1606            t_pts = [Vector(p) for p in [
1607                (0.06, -1.0, 1.0), (0.19, -1.0, 0.5), (0.31, -1.0, 0.5), (0.44, -1.0, 1.0),
1608                (0.56, -1.0, 1.0), (0.69, -1.0, 0.5), (0.81, -1.0, 0.5), (0.94, -1.0, 1.0),
1609                (0.06, 0.0, 0.5), (0.19, 0.0, 0.0), (0.31, 0.0, 0.0), (0.44, 0.0, 0.5),
1610                (0.56, 0.0, 0.5), (0.69, 0.0, 0.0), (0.81, 0.0, 0.0), (0.94, 0.0, 0.5),
1611                (-0.0, -1.0, 1.0), (-0.0, 0.0, 0.5), (1.0, -1.0, 1.0), (1.0, 0.0, 0.5)]]
1612            t_faces = [
1613                (16, 0, 8, 17), (0, 1, 9, 8), (1, 2, 10, 9), (2, 3, 11, 10),
1614                (3, 4, 12, 11), (4, 5, 13, 12), (5, 6, 14, 13), (6, 7, 15, 14), (7, 18, 19, 15)]
1615        elif d.tile_model == 'BRAAS1':
1616            t_pts = [Vector(p) for p in [
1617                (0.1, -1.0, 1.0), (0.2, -1.0, 0.5), (0.6, -1.0, 0.5), (0.7, -1.0, 1.0),
1618                (0.1, 0.0, 0.5), (0.2, 0.0, 0.0), (0.6, 0.0, 0.0), (0.7, 0.0, 0.5),
1619                (-0.0, -1.0, 1.0), (-0.0, 0.0, 0.5), (1.0, -1.0, 1.0), (1.0, 0.0, 0.5)]]
1620            t_faces = [(8, 0, 4, 9), (0, 1, 5, 4), (1, 2, 6, 5), (2, 3, 7, 6), (3, 10, 11, 7)]
1621        elif d.tile_model == 'ETERNIT':
1622            t_pts = [Vector(p) for p in [
1623                (0.11, -1.0, 1.0), (0.9, -1.0, 1.0), (0.0, -0.79, 0.79),
1624                (1.0, -0.79, 0.79), (0.0, 2.0, -2.0), (1.0, 2.0, -2.0)]]
1625            t_faces = [(0, 1, 3, 5, 4, 2)]
1626        elif d.tile_model == 'ONDULEE':
1627            t_pts = [Vector(p) for p in [
1628                (0.0, -1.0, 0.1), (0.05, -1.0, 1.0), (0.1, -1.0, 0.1),
1629                (0.15, -1.0, 1.0), (0.2, -1.0, 0.1), (0.25, -1.0, 1.0),
1630                (0.3, -1.0, 0.1), (0.35, -1.0, 1.0), (0.4, -1.0, 0.1),
1631                (0.45, -1.0, 1.0), (0.5, -1.0, 0.1), (0.55, -1.0, 1.0),
1632                (0.6, -1.0, 0.1), (0.65, -1.0, 1.0), (0.7, -1.0, 0.1),
1633                (0.75, -1.0, 1.0), (0.8, -1.0, 0.1), (0.85, -1.0, 1.0),
1634                (0.9, -1.0, 0.1), (0.95, -1.0, 1.0), (1.0, -1.0, 0.1),
1635                (0.0, 0.0, 0.0), (0.05, 0.0, 0.9), (0.1, 0.0, 0.0),
1636                (0.15, 0.0, 0.9), (0.2, 0.0, 0.0), (0.25, 0.0, 0.9),
1637                (0.3, 0.0, 0.0), (0.35, 0.0, 0.9), (0.4, 0.0, 0.0),
1638                (0.45, 0.0, 0.9), (0.5, 0.0, 0.0), (0.55, 0.0, 0.9),
1639                (0.6, 0.0, 0.0), (0.65, 0.0, 0.9), (0.7, 0.0, 0.0),
1640                (0.75, 0.0, 0.9), (0.8, 0.0, 0.0), (0.85, 0.0, 0.9),
1641                (0.9, 0.0, 0.0), (0.95, 0.0, 0.9), (1.0, 0.0, 0.0)]]
1642            t_faces = [
1643                (0, 1, 22, 21), (1, 2, 23, 22), (2, 3, 24, 23),
1644                (3, 4, 25, 24), (4, 5, 26, 25), (5, 6, 27, 26),
1645                (6, 7, 28, 27), (7, 8, 29, 28), (8, 9, 30, 29),
1646                (9, 10, 31, 30), (10, 11, 32, 31), (11, 12, 33, 32),
1647                (12, 13, 34, 33), (13, 14, 35, 34), (14, 15, 36, 35),
1648                (15, 16, 37, 36), (16, 17, 38, 37), (17, 18, 39, 38),
1649                (18, 19, 40, 39), (19, 20, 41, 40)]
1650        elif d.tile_model == 'METAL':
1651            t_pts = [Vector(p) for p in [
1652                (0.0, -1.0, 0.0), (0.99, -1.0, 0.0), (1.0, -1.0, 0.0),
1653                (0.0, 0.0, 0.0), (0.99, 0.0, 0.0), (1.0, 0.0, 0.0),
1654                (0.99, -1.0, 1.0), (1.0, -1.0, 1.0), (1.0, 0.0, 1.0), (0.99, 0.0, 1.0)]]
1655            t_faces = [(0, 1, 4, 3), (7, 2, 5, 8), (1, 6, 9, 4), (6, 7, 8, 9)]
1656        elif d.tile_model == 'LAUZE':
1657            t_pts = [Vector(p) for p in [
1658                (0.75, -0.8, 0.8), (0.5, -1.0, 1.0), (0.25, -0.8, 0.8),
1659                (0.0, -0.5, 0.5), (1.0, -0.5, 0.5), (0.0, 0.5, -0.5), (1.0, 0.5, -0.5)]]
1660            t_faces = [(1, 0, 4, 6, 5, 3, 2)]
1661        elif d.tile_model == 'PLACEHOLDER':
1662            t_pts = [Vector(p) for p in [(0.0, -1.0, 1.0), (1.0, -1.0, 1.0), (0.0, 0.0, 0.0), (1.0, 0.0, 0.0)]]
1663            t_faces = [(0, 1, 3, 2)]
1664        elif d.tile_model == 'ROMAN':
1665            t_pts = [Vector(p) for p in [
1666                (0.18, 0.0, 0.3), (0.24, 0.0, 0.58), (0.76, 0.0, 0.58),
1667                (0.82, 0.0, 0.3), (0.05, -1.0, 0.5), (0.14, -1.0, 0.8),
1668                (0.86, -1.0, 0.8), (0.95, -1.0, 0.5), (0.45, 0.0, 0.5),
1669                (0.36, 0.0, 0.2), (-0.36, 0.0, 0.2), (-0.45, -0.0, 0.5),
1670                (0.32, -1.0, 0.7), (0.26, -1.0, 0.42), (-0.26, -1.0, 0.42),
1671                (-0.32, -1.0, 0.7), (0.5, 0.0, 0.74), (0.5, -1.0, 1.0),
1672                (-0.0, -1.0, 0.26), (-0.0, 0.0, 0.0)]
1673            ]
1674            t_faces = [
1675                (0, 4, 5, 1), (16, 17, 6, 2), (2, 6, 7, 3),
1676                (13, 12, 8, 9), (18, 13, 9, 19), (15, 14, 10, 11),
1677                (14, 18, 19, 10), (1, 5, 17, 16)
1678            ]
1679        elif d.tile_model == 'ROUND':
1680            t_pts = [Vector(p) for p in [
1681                (0.0, -0.5, 0.5), (1.0, -0.5, 0.5), (0.0, 0.0, 0.0),
1682                (1.0, 0.0, 0.0), (0.93, -0.71, 0.71), (0.78, -0.88, 0.88),
1683                (0.39, -0.97, 0.97), (0.61, -0.97, 0.97), (0.07, -0.71, 0.71),
1684                (0.22, -0.88, 0.88)]
1685            ]
1686            t_faces = [(6, 7, 5, 4, 1, 3, 2, 0, 8, 9)]
1687        else:
1688            return
1689
1690        n_faces = len(t_faces)
1691        t_uvs = [[(t_pts[i].x, t_pts[i].y) for i in f] for f in t_faces]
1692
1693        dx, dy = d.tile_space_x, d.tile_space_y
1694
1695        step = 100 / ttl
1696
1697        # if d.quick_edit:
1698        #    context.scene.archipack_progress_text = "Build tiles:"
1699
1700        for i, pan in enumerate(self.pans):
1701
1702            seg = pan.fake_axis
1703            # compute base matrix top left of face
1704            vx = pan.vx
1705            vy = pan.vy
1706            vz = pan.vz
1707
1708            x0, y0 = seg.lerp(pan.tmax)
1709            z0 = self.z + d.tile_altitude
1710            ysize_2d = (d.tile_border + pan.ysize)
1711            space_x = pan.xsize + 2 * d.tile_side
1712            space_y = ysize_2d * sqrt(1 + pan.slope * pan.slope)
1713            n_x = 1 + int(space_x / dx)
1714            n_y = 1 + int(space_y / dy)
1715
1716            if d.tile_fit_x:
1717                dx = space_x / n_x
1718
1719            if d.tile_fit_y:
1720                dy = space_y / n_y
1721
1722            if d.tile_alternate:
1723                n_y += 1
1724
1725            tM = Matrix([
1726                [vx.x, vy.x, vz.x, x0],
1727                [vx.y, vy.y, vz.y, y0],
1728                [vx.z, vy.z, vz.z, z0],
1729                [0, 0, 0, 1]
1730            ])
1731
1732            verts = []
1733            faces = []
1734            matids = []
1735            uvs = []
1736
1737            # steps for this pan
1738            substep = step / n_y
1739            # print("step:%s sub:%s" % (step, substep))
1740
1741            for k in range(n_y):
1742
1743                progress = step * i + substep * k
1744                # print("progress %s" % (progress))
1745                # if d.quick_edit:
1746                #    context.scene.archipack_progress = progress
1747
1748                y = k * dy
1749
1750                x0 = offset * dx - d.tile_side
1751                nx = n_x
1752
1753                if d.tile_alternate and k % 2 == 1:
1754                    x0 -= 0.5 * dx
1755                    nx += 1
1756
1757                if d.tile_offset > 0:
1758                    nx += 1
1759
1760                for j in range(nx):
1761                    x = x0 + j * dx
1762                    lM = tM @ Matrix([
1763                        [sx, 0, 0, x],
1764                        [0, sy, 0, -y],
1765                        [0, 0, sz, 0],
1766                        [0, 0, 0, 1]
1767                    ])
1768
1769                    v = len(verts)
1770
1771                    verts.extend([lM @ p for p in t_pts])
1772                    faces.extend([tuple(i + v for i in f) for f in t_faces])
1773                    mid = randint(idmat, idmat + rand)
1774                    t_mats = [mid for i in range(n_faces)]
1775                    matids.extend(t_mats)
1776                    uvs.extend(t_uvs)
1777
1778            # build temp bmesh and bissect
1779            bm = bmed.buildmesh(
1780                context, o, verts, faces, matids=matids, uvs=uvs,
1781                weld=False, clean=False, auto_smooth=True, temporary=True)
1782
1783            # clean outer on convex parts
1784            # pan.convex = False
1785            remove = pan.convex
1786
1787            for s in pan.segs:
1788                # seg without length lead to invalid normal
1789                if s.length > 0:
1790                    if s.type == 'AXIS':
1791                        self.bissect(bm, s.p1.to_3d(), s.cross_z.to_3d(), clear_outer=remove)
1792                    elif s.type == 'BOTTOM':
1793                        s0 = s.offset(d.tile_border)
1794                        dz = pan.altitude(s0.p0)
1795                        vx = s0.v.to_3d()
1796                        vx.z = pan.altitude(s0.p1) - dz
1797                        vy = vz.cross(vx.normalized())
1798                        x, y = s0.p0
1799                        z = z0 + dz
1800                        self.bissect(bm, Vector((x, y, z)), -vy, clear_outer=remove)
1801                    elif s.type == 'SIDE':
1802                        p0 = s.p0 + s.cross_z.normalized() * d.tile_side
1803                        self.bissect(bm, p0.to_3d(), s.cross_z.to_3d(), clear_outer=remove)
1804                    elif s.type == 'LINK_VALLEY':
1805                        p0 = s.p0 - s.cross_z.normalized() * d.tile_couloir
1806                        self.bissect(bm, p0.to_3d(), s.cross_z.to_3d(), clear_outer=remove)
1807                    elif s.type in {'LINK_HIP', 'LINK'}:
1808                        self.bissect(bm, s.p0.to_3d(), s.cross_z.to_3d(), clear_outer=remove)
1809
1810            # when not convex, select and remove outer parts
1811            if not pan.convex:
1812                """
1813                /* del "context" slot values, used for operator too */
1814                enum {
1815                    DEL_VERTS = 1,
1816                    DEL_EDGES,
1817                    DEL_ONLYFACES,
1818                    DEL_EDGESFACES,
1819                    DEL_FACES,
1820                    /* A version of 'DEL_FACES' that keeps edges on face boundaries,
1821                     * allowing the surrounding edge-loop to be kept from removed face regions. */
1822                    DEL_FACES_KEEP_BOUNDARY,
1823                    DEL_ONLYTAGGED
1824                };
1825                """
1826                # Build boundary including borders and bottom offsets
1827                new_s = None
1828                segs = []
1829                for s in pan.segs:
1830                    if s.length > 0:
1831                        if s.type == 'LINK_VALLEY':
1832                            offset = -d.tile_couloir
1833                        elif s.type == 'BOTTOM':
1834                            offset = d.tile_border
1835                        elif s.type == 'SIDE':
1836                            offset = d.tile_side
1837                        else:
1838                            offset = 0
1839                        new_s = s.make_offset(offset, new_s)
1840                        segs.append(new_s)
1841
1842                if len(segs) > 0:
1843                    # last / first intersection
1844                    res, p, t = segs[0].intersect(segs[-1])
1845                    if res:
1846                        segs[0].p0 = p
1847                        segs[-1].p1 = p
1848                    f_geom = [f for f in bm.faces if not pan.inside(f.calc_center_median().to_2d(), segs)]
1849                    if len(f_geom) > 0:
1850                        bmesh.ops.delete(bm, geom=f_geom, context="FACES")
1851
1852            self.cut_holes(bm, pan)
1853
1854            bmesh.ops.dissolve_limit(bm,
1855                        angle_limit=0.01,
1856                        use_dissolve_boundaries=False,
1857                        verts=bm.verts[:],
1858                        edges=bm.edges[:],
1859                        delimit={'MATERIAL'})
1860
1861            if d.tile_bevel:
1862                geom = bm.verts[:]
1863                geom.extend(bm.edges[:])
1864                bmesh.ops.bevel(bm,
1865                    geom=geom,
1866                    offset=d.tile_bevel_amt,
1867                    offset_type=offset_type,
1868                    segments=d.tile_bevel_segs,
1869                    profile=0.5,
1870                    # vertex_only=False,
1871                    clamp_overlap=True,
1872                    material=-1)
1873
1874            if d.tile_solidify:
1875                geom = bm.faces[:]
1876                verts = bm.verts[:]
1877                bmesh.ops.solidify(bm, geom=geom, thickness=0.0001)
1878                bmesh.ops.translate(bm, vec=vz * d.tile_height, space=o.matrix_world, verts=verts)
1879
1880            # merge with object
1881            bmed.bmesh_join(context, o, [bm], normal_update=True)
1882            bpy.ops.object.mode_set(mode='OBJECT')
1883
1884        # if d.quick_edit:
1885        #    context.scene.archipack_progress = -1
1886
1887    def _bargeboard(self, s, i, boundary, pan,
1888            width, height, altitude, offset, idmat,
1889            verts, faces, edges, matids, uvs):
1890
1891        f = len(verts)
1892
1893        s0 = s.offset(offset - width)
1894        s1 = s.offset(offset)
1895
1896        p0 = s0.p0
1897        p1 = s1.p0
1898        p2 = s0.p1
1899        p3 = s1.p1
1900
1901        s2 = boundary.last_seg(i)
1902        s3 = boundary.next_seg(i)
1903
1904        if s2.type == 'SIDE':
1905            # intersect last seg offset
1906            s4 = s2.offset(offset - width)
1907            s5 = s2.offset(offset)
1908            res, p, t = s4.intersect(s0)
1909            if res:
1910                p0 = p
1911            res, p, t = s5.intersect(s1)
1912            if res:
1913                p1 = p
1914
1915        elif s2.type == 'AXIS' or 'LINK' in s2.type:
1916            # intersect axis or link seg
1917            res, p, t = s2.intersect(s0)
1918            if res:
1919                p0 = p
1920            res, p, t = s2.intersect(s1)
1921            if res:
1922                p1 = p
1923
1924        if s3.type == 'SIDE':
1925            # intersect next seg offset
1926            s4 = s3.offset(offset - width)
1927            s5 = s3.offset(offset)
1928            res, p, t = s4.intersect(s0)
1929            if res:
1930                p2 = p
1931            res, p, t = s5.intersect(s1)
1932            if res:
1933                p3 = p
1934
1935        elif s3.type == 'AXIS' or 'LINK' in s3.type:
1936            # intersect axis or link seg
1937            res, p, t = s3.intersect(s0)
1938            if res:
1939                p2 = p
1940            res, p, t = s3.intersect(s1)
1941            if res:
1942                p3 = p
1943
1944        x0, y0 = p0
1945        x1, y1 = p1
1946        x2, y2 = p3
1947        x3, y3 = p2
1948
1949        z0 = self.z + altitude + pan.altitude(p0)
1950        z1 = self.z + altitude + pan.altitude(p1)
1951        z2 = self.z + altitude + pan.altitude(p3)
1952        z3 = self.z + altitude + pan.altitude(p2)
1953
1954        verts.extend([
1955            (x0, y0, z0),
1956            (x1, y1, z1),
1957            (x2, y2, z2),
1958            (x3, y3, z3),
1959        ])
1960        z0 -= height
1961        z1 -= height
1962        z2 -= height
1963        z3 -= height
1964        verts.extend([
1965            (x0, y0, z0),
1966            (x1, y1, z1),
1967            (x2, y2, z2),
1968            (x3, y3, z3),
1969        ])
1970
1971        faces.extend([
1972            # top
1973            (f, f + 1, f + 2, f + 3),
1974            # sides
1975            (f, f + 4, f + 5, f + 1),
1976            (f + 1, f + 5, f + 6, f + 2),
1977            (f + 2, f + 6, f + 7, f + 3),
1978            (f + 3, f + 7, f + 4, f),
1979            # bottom
1980            (f + 4, f + 7, f + 6, f + 5)
1981        ])
1982        edges.append([f, f + 3])
1983        edges.append([f + 1, f + 2])
1984        edges.append([f + 4, f + 7])
1985        edges.append([f + 5, f + 6])
1986
1987        matids.extend([idmat, idmat, idmat, idmat, idmat, idmat])
1988        uvs.extend([
1989            [(0, 0), (0, 1), (1, 1), (1, 0)],
1990            [(0, 0), (0, 1), (1, 1), (1, 0)],
1991            [(0, 0), (0, 1), (1, 1), (1, 0)],
1992            [(0, 0), (0, 1), (1, 1), (1, 0)],
1993            [(0, 0), (0, 1), (1, 1), (1, 0)],
1994            [(0, 0), (0, 1), (1, 1), (1, 0)]
1995        ])
1996
1997    def bargeboard(self, d, verts, faces, edges, matids, uvs):
1998
1999        #####################
2000        # Vire-vents
2001        #####################
2002
2003        idmat = 1
2004        for pan in self.pans:
2005
2006            for hole in pan.holes:
2007                for i, s in enumerate(hole.segs):
2008                    if s.type == 'SIDE':
2009                        self._bargeboard(s,
2010                            i,
2011                            hole, pan,
2012                            d.bargeboard_width,
2013                            d.bargeboard_height,
2014                            d.bargeboard_altitude,
2015                            d.bargeboard_offset,
2016                            idmat,
2017                            verts,
2018                            faces,
2019                            edges,
2020                            matids,
2021                            uvs)
2022
2023            for i, s in enumerate(pan.segs):
2024                if s.type == 'SIDE':
2025                    self._bargeboard(s,
2026                        i,
2027                        pan, pan,
2028                        d.bargeboard_width,
2029                        d.bargeboard_height,
2030                        d.bargeboard_altitude,
2031                        d.bargeboard_offset,
2032                        idmat,
2033                        verts,
2034                        faces,
2035                        edges,
2036                        matids,
2037                        uvs)
2038
2039    def _fascia(self, s, i, boundary, pan, tri_0, tri_1,
2040            width, height, altitude, offset, idmat,
2041            verts, faces, edges, matids, uvs):
2042
2043        f = len(verts)
2044        s0 = s.offset(offset)
2045        s1 = s.offset(offset + width)
2046
2047        s2 = boundary.last_seg(i)
2048        s3 = boundary.next_seg(i)
2049        s4 = s2
2050        s5 = s3
2051
2052        p0 = s0.p0
2053        p1 = s1.p0
2054        p2 = s0.p1
2055        p3 = s1.p1
2056
2057        # find last neighbor depending on type
2058        if s2.type == 'AXIS' or 'LINK' in s2.type:
2059            # apply only on boundaries
2060            if not s.is_hole:
2061                # use last axis
2062                if pan.side == 'LEFT':
2063                    s6 = pan.next_cross
2064                else:
2065                    s6 = pan.last_cross
2066                if tri_0:
2067                    s2 = s.copy
2068                else:
2069                    s2 = s2.oposite
2070                s2.v = (s.sized_normal(0, 1).v + s6.v).normalized()
2071                s4 = s2
2072
2073        elif s2.type == 'SIDE':
2074            s2 = s.copy
2075            s2.type = 'SIDE'
2076            s2.v = s.sized_normal(0, 1).v
2077            s4 = s2
2078        else:
2079            s2 = s2.offset(offset)
2080            s4 = s2.offset(offset + width)
2081
2082        # find next neighbor depending on type
2083        if s3.type == 'AXIS' or 'LINK' in s3.type:
2084            if not s.is_hole:
2085                # use last axis
2086                if pan.side == 'LEFT':
2087                    s6 = pan.last_cross
2088                else:
2089                    s6 = pan.next_cross
2090                if tri_1:
2091                    s3 = s.oposite
2092                else:
2093                    s3 = s3.copy
2094                s3.v = (s.sized_normal(0, 1).v + s6.v).normalized()
2095                s5 = s3
2096        elif s3.type == 'SIDE':
2097            # when next is side, use perpendicular
2098            s3 = s.oposite
2099            s3.type = 'SIDE'
2100            s3.v = s.sized_normal(0, 1).v
2101            s5 = s3
2102        else:
2103            s3 = s3.offset(offset)
2104            s5 = s3.offset(offset + width)
2105
2106        # units vectors and scale
2107        # is unit normal on sides
2108        # print("s.p:%s, s.v:%s s1.p::%s s1.v::%s" % (s.p, s.v, s1.p, s1.v))
2109        res, p, t = s0.intersect(s2)
2110        if res:
2111            p0 = p
2112        res, p, t = s0.intersect(s3)
2113        if res:
2114            p1 = p
2115        res, p, t = s1.intersect(s4)
2116        if res:
2117            p2 = p
2118        res, p, t = s1.intersect(s5)
2119        if res:
2120            p3 = p
2121
2122        x0, y0 = p0
2123        x1, y1 = p2
2124        x2, y2 = p3
2125        x3, y3 = p1
2126
2127        z0 = self.z + altitude + pan.altitude(p0)
2128        z1 = self.z + altitude + pan.altitude(p2)
2129        z2 = self.z + altitude + pan.altitude(p3)
2130        z3 = self.z + altitude + pan.altitude(p1)
2131
2132        verts.extend([
2133            (x0, y0, z0),
2134            (x1, y1, z1),
2135            (x2, y2, z2),
2136            (x3, y3, z3),
2137        ])
2138
2139        z0 -= height
2140        z1 -= height
2141        z2 -= height
2142        z3 -= height
2143        verts.extend([
2144            (x0, y0, z0),
2145            (x1, y1, z1),
2146            (x2, y2, z2),
2147            (x3, y3, z3),
2148        ])
2149
2150        faces.extend([
2151            # top
2152            (f, f + 1, f + 2, f + 3),
2153            # sides
2154            (f, f + 4, f + 5, f + 1),
2155            (f + 1, f + 5, f + 6, f + 2),
2156            (f + 2, f + 6, f + 7, f + 3),
2157            (f + 3, f + 7, f + 4, f),
2158            # bottom
2159            (f + 4, f + 7, f + 6, f + 5)
2160        ])
2161        edges.append([f, f + 3])
2162        edges.append([f + 1, f + 2])
2163        edges.append([f + 4, f + 7])
2164        edges.append([f + 5, f + 6])
2165        matids.extend([idmat, idmat, idmat, idmat, idmat, idmat])
2166        uvs.extend([
2167            [(0, 0), (0, 1), (1, 1), (1, 0)],
2168            [(0, 0), (0, 1), (1, 1), (1, 0)],
2169            [(0, 0), (0, 1), (1, 1), (1, 0)],
2170            [(0, 0), (0, 1), (1, 1), (1, 0)],
2171            [(0, 0), (0, 1), (1, 1), (1, 0)],
2172            [(0, 0), (0, 1), (1, 1), (1, 0)]
2173        ])
2174
2175    def fascia(self, d, verts, faces, edges, matids, uvs):
2176
2177        #####################
2178        # Larmiers
2179        #####################
2180
2181        idmat = 2
2182        for pan in self.pans:
2183
2184            for hole in pan.holes:
2185                for i, s in enumerate(hole.segs):
2186                    if s.type == 'BOTTOM':
2187                        self._fascia(s,
2188                            i,
2189                            hole, pan,
2190                            False, False,
2191                            d.fascia_width,
2192                            d.fascia_height,
2193                            d.fascia_altitude,
2194                            d.fascia_offset,
2195                            idmat,
2196                            verts,
2197                            faces,
2198                            edges,
2199                            matids,
2200                            uvs)
2201
2202            for i, s in enumerate(pan.segs):
2203                if s.type == 'BOTTOM':
2204
2205                    tri_0 = pan.node_tri
2206                    tri_1 = pan.next_tri
2207
2208                    # triangular ends apply on boundary only
2209                    # unless cut, boundary is parallel to axis
2210                    # except for triangular ends
2211                    if pan.side == 'LEFT':
2212                        tri_0, tri_1 = tri_1, tri_0
2213
2214                    self._fascia(s,
2215                        i,
2216                        pan, pan,
2217                        tri_0, tri_1,
2218                        d.fascia_width,
2219                        d.fascia_height,
2220                        d.fascia_altitude,
2221                        d.fascia_offset,
2222                        idmat,
2223                        verts,
2224                        faces,
2225                        edges,
2226                        matids,
2227                        uvs)
2228
2229                    continue
2230
2231                    f = len(verts)
2232                    s0 = s.offset(d.fascia_width)
2233
2234                    s1 = pan.last_seg(i)
2235                    s2 = pan.next_seg(i)
2236
2237                    # triangular ends apply on boundary only
2238                    # unless cut, boundary is parallel to axis
2239                    # except for triangular ends
2240
2241                    tri_0 = (pan.node_tri and not s.is_hole) or pan.is_tri
2242                    tri_1 = (pan.next_tri and not s.is_hole) or pan.is_tri
2243
2244                    if pan.side == 'LEFT':
2245                        tri_0, tri_1 = tri_1, tri_0
2246
2247                    # tiangular use bottom segment direction
2248                    # find last neighbor depending on type
2249                    if s1.type == 'AXIS' or 'LINK' in s1.type:
2250                        # apply only on boundaries
2251                        if not s.is_hole:
2252                            # use last axis
2253                            if pan.side == 'LEFT':
2254                                s3 = pan.next_cross
2255                            else:
2256                                s3 = pan.last_cross
2257                            if tri_0:
2258                                s1 = s.copy
2259                            else:
2260                                s1 = s1.oposite
2261                            s1.v = (s.sized_normal(0, 1).v + s3.v).normalized()
2262                    elif s1.type == 'SIDE':
2263                        s1 = s.copy
2264                        s1.type = 'SIDE'
2265                        s1.v = s.sized_normal(0, 1).v
2266                    else:
2267                        s1 = s1.offset(d.fascia_width)
2268
2269                    # find next neighbor depending on type
2270                    if s2.type == 'AXIS' or 'LINK' in s2.type:
2271                        if not s.is_hole:
2272                            # use last axis
2273                            if pan.side == 'LEFT':
2274                                s3 = pan.last_cross
2275                            else:
2276                                s3 = pan.next_cross
2277                            if tri_1:
2278                                s2 = s.oposite
2279                            else:
2280                                s2 = s2.copy
2281                            s2.v = (s.sized_normal(0, 1).v + s3.v).normalized()
2282                    elif s2.type == 'SIDE':
2283                        s2 = s.oposite
2284                        s2.type = 'SIDE'
2285                        s2.v = s.sized_normal(0, 1).v
2286                    else:
2287
2288                        s2 = s2.offset(d.fascia_width)
2289
2290                    # units vectors and scale
2291                    # is unit normal on sides
2292                    # print("s.p:%s, s.v:%s s1.p::%s s1.v::%s" % (s.p, s.v, s1.p, s1.v))
2293                    res, p0, t = s0.intersect(s1)
2294                    res, p1, t = s0.intersect(s2)
2295
2296                    x0, y0 = s.p0
2297                    x1, y1 = p0
2298                    x2, y2 = p1
2299                    x3, y3 = s.p1
2300                    z0 = self.z + d.fascia_altitude + pan.altitude(s.p0)
2301                    z1 = self.z + d.fascia_altitude + pan.altitude(s.p1)
2302                    verts.extend([
2303                        (x0, y0, z0),
2304                        (x1, y1, z0),
2305                        (x2, y2, z1),
2306                        (x3, y3, z1),
2307                    ])
2308                    z0 -= d.fascia_height
2309                    z1 -= d.fascia_height
2310                    verts.extend([
2311                        (x0, y0, z0),
2312                        (x1, y1, z0),
2313                        (x2, y2, z1),
2314                        (x3, y3, z1),
2315                    ])
2316
2317                    faces.extend([
2318                        # top
2319                        (f, f + 1, f + 2, f + 3),
2320                        # sides
2321                        (f, f + 4, f + 5, f + 1),
2322                        (f + 1, f + 5, f + 6, f + 2),
2323                        (f + 2, f + 6, f + 7, f + 3),
2324                        (f + 3, f + 7, f + 4, f),
2325                        # bottom
2326                        (f + 4, f + 7, f + 6, f + 5)
2327                    ])
2328                    edges.append([f, f + 3])
2329                    edges.append([f + 1, f + 2])
2330                    edges.append([f + 4, f + 7])
2331                    edges.append([f + 5, f + 6])
2332                    matids.extend([idmat, idmat, idmat, idmat, idmat, idmat])
2333                    uvs.extend([
2334                        [(0, 0), (0, 1), (1, 1), (1, 0)],
2335                        [(0, 0), (0, 1), (1, 1), (1, 0)],
2336                        [(0, 0), (0, 1), (1, 1), (1, 0)],
2337                        [(0, 0), (0, 1), (1, 1), (1, 0)],
2338                        [(0, 0), (0, 1), (1, 1), (1, 0)],
2339                        [(0, 0), (0, 1), (1, 1), (1, 0)]
2340                    ])
2341
2342    def gutter(self, d, verts, faces, edges, matids, uvs):
2343
2344        #####################
2345        # Chenaux
2346        #####################
2347
2348        idmat = 5
2349
2350        # caps at start and end
2351        if d.gutter_segs % 2 == 1:
2352            n_faces = int((d.gutter_segs - 1) / 2)
2353        else:
2354            n_faces = int((d.gutter_segs / 2) - 1)
2355
2356        df = 2 * d.gutter_segs + 1
2357
2358        for pan in self.pans:
2359            for i, s in enumerate(pan.segs):
2360
2361                if s.type == 'BOTTOM':
2362                    f = len(verts)
2363
2364                    s0 = s.offset(d.gutter_dist + d.gutter_width)
2365
2366                    s1 = pan.last_seg(i)
2367                    s2 = pan.next_seg(i)
2368
2369                    p0 = s0.p0
2370                    p1 = s0.p1
2371
2372                    tri_0 = pan.node_tri or pan.is_tri
2373                    tri_1 = pan.next_tri or pan.is_tri
2374
2375                    if pan.side == 'LEFT':
2376                        tri_0, tri_1 = tri_1, tri_0
2377
2378                    f = len(verts)
2379
2380                    # tiangular use segment direction
2381                    # find last neighbor depending on type
2382                    if s1.type == 'AXIS' or 'LINK' in s1.type:
2383                        # apply only on boundaries
2384                        if not s.is_hole:
2385                            # use last axis
2386                            if pan.side == 'LEFT':
2387                                s3 = pan.next_cross
2388                            else:
2389                                s3 = pan.last_cross
2390                            if tri_0:
2391                                s1 = s.copy
2392                            else:
2393                                s1 = s1.oposite
2394                            s1.v = (s.sized_normal(0, 1).v + s3.v).normalized()
2395                    elif s1.type == 'SIDE':
2396                        s1 = s.copy
2397                        s1.type = 'SIDE'
2398                        s1.v = s.sized_normal(0, 1).v
2399                    else:
2400                        s1 = s1.offset(d.gutter_dist + d.gutter_width)
2401
2402                    # find next neighbor depending on type
2403                    if s2.type == 'AXIS' or 'LINK' in s2.type:
2404                        if not s.is_hole:
2405                            # use last axis
2406                            if pan.side == 'LEFT':
2407                                s3 = pan.last_cross
2408                            else:
2409                                s3 = pan.next_cross
2410                            if tri_1:
2411                                s2 = s.oposite
2412                            else:
2413                                s2 = s2.copy
2414                            s2.v = (s.sized_normal(0, 1).v + s3.v).normalized()
2415                    elif s2.type == 'SIDE':
2416                        s2 = s.oposite
2417                        s2.type = 'SIDE'
2418                        s2.v = s.sized_normal(0, 1).v
2419                    else:
2420                        s2 = s2.offset(d.gutter_dist + d.gutter_width)
2421
2422                    # units vectors and scale
2423                    # is unit normal on sides
2424                    # print("s.p:%s, s.v:%s s1.p::%s s1.v::%s" % (s.p, s.v, s1.p, s1.v))
2425                    res, p, t = s0.intersect(s1)
2426                    if res:
2427                        p0 = p
2428                    res, p, t = s0.intersect(s2)
2429                    if res:
2430                        p1 = p
2431                    """
2432                    f = len(verts)
2433                    verts.extend([s1.p0.to_3d(), s1.p1.to_3d()])
2434                    edges.append([f, f + 1])
2435
2436                    f = len(verts)
2437                    verts.extend([s2.p0.to_3d(), s2.p1.to_3d()])
2438                    edges.append([f, f + 1])
2439                    continue
2440                    """
2441
2442                    v0 = p0 - s.p0
2443                    v1 = p1 - s.p1
2444
2445                    scale_0 = v0.length / (d.gutter_dist + d.gutter_width)
2446                    scale_1 = v1.length / (d.gutter_dist + d.gutter_width)
2447
2448                    s3 = Line(s.p0, v0.normalized())
2449                    s4 = Line(s.p1, v1.normalized())
2450
2451                    zt = self.z + d.fascia_altitude + pan.altitude(s3.p0)
2452                    z0 = self.z + d.gutter_alt + pan.altitude(s3.p0)
2453                    z1 = z0 - 0.5 * d.gutter_width
2454                    z2 = z1 - 0.5 * d.gutter_width
2455                    z3 = z1 - 0.5 * d.gutter_boudin
2456                    dz0 = z2 - z1
2457                    dz1 = z3 - z1
2458
2459                    tt = scale_0 * d.fascia_width
2460                    t0 = scale_0 * d.gutter_dist
2461                    t1 = t0 + scale_0 * (0.5 * d.gutter_width)
2462                    t2 = t1 + scale_0 * (0.5 * d.gutter_width)
2463                    t3 = t2 + scale_0 * (0.5 * d.gutter_boudin)
2464
2465                    # bord tablette
2466                    xt, yt = s3.lerp(tt)
2467
2468                    # bord
2469                    x0, y0 = s3.lerp(t0)
2470                    # axe chenaux
2471                    x1, y1 = s3.lerp(t1)
2472                    # bord boudin interieur
2473                    x2, y2 = s3.lerp(t2)
2474                    # axe boudin
2475                    x3, y3 = s3.lerp(t3)
2476
2477                    dx = x0 - x1
2478                    dy = y0 - y1
2479
2480                    verts.append((xt, yt, zt))
2481                    # chenaux
2482                    da = pi / d.gutter_segs
2483                    for i in range(d.gutter_segs):
2484                        sa = sin(i * da)
2485                        ca = cos(i * da)
2486                        verts.append((x1 + dx * ca, y1 + dy * ca, z1 + dz0 * sa))
2487
2488                    dx = x2 - x3
2489                    dy = y2 - y3
2490
2491                    # boudin
2492                    da = -pi / (0.75 * d.gutter_segs)
2493                    for i in range(d.gutter_segs):
2494                        sa = sin(i * da)
2495                        ca = cos(i * da)
2496                        verts.append((x3 + dx * ca, y3 + dy * ca, z1 + dz1 * sa))
2497
2498                    zt = self.z + d.fascia_altitude + pan.altitude(s4.p0)
2499                    z0 = self.z + d.gutter_alt + pan.altitude(s4.p0)
2500                    z1 = z0 - 0.5 * d.gutter_width
2501                    z2 = z1 - 0.5 * d.gutter_width
2502                    z3 = z1 - 0.5 * d.gutter_boudin
2503                    dz0 = z2 - z1
2504                    dz1 = z3 - z1
2505                    tt = scale_1 * d.fascia_width
2506                    t0 = scale_1 * d.gutter_dist
2507                    t1 = t0 + scale_1 * (0.5 * d.gutter_width)
2508                    t2 = t1 + scale_1 * (0.5 * d.gutter_width)
2509                    t3 = t2 + scale_1 * (0.5 * d.gutter_boudin)
2510
2511                    # bord tablette
2512                    xt, yt = s4.lerp(tt)
2513
2514                    # bord
2515                    x0, y0 = s4.lerp(t0)
2516                    # axe chenaux
2517                    x1, y1 = s4.lerp(t1)
2518                    # bord boudin interieur
2519                    x2, y2 = s4.lerp(t2)
2520                    # axe boudin
2521                    x3, y3 = s4.lerp(t3)
2522
2523                    dx = x0 - x1
2524                    dy = y0 - y1
2525
2526                    # tablette
2527                    verts.append((xt, yt, zt))
2528                    faces.append((f + df, f, f + 1, f + df + 1))
2529                    uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)])
2530                    matids.append(idmat)
2531
2532                    # chenaux
2533                    da = pi / d.gutter_segs
2534                    for i in range(d.gutter_segs):
2535                        sa = sin(i * da)
2536                        ca = cos(i * da)
2537                        verts.append((x1 + dx * ca, y1 + dy * ca, z1 + dz0 * sa))
2538
2539                    dx = x2 - x3
2540                    dy = y2 - y3
2541
2542                    # boudin
2543                    da = -pi / (0.75 * d.gutter_segs)
2544                    for i in range(d.gutter_segs):
2545                        sa = sin(i * da)
2546                        ca = cos(i * da)
2547                        verts.append((x3 + dx * ca, y3 + dy * ca, z1 + dz1 * sa))
2548
2549                    df = 2 * d.gutter_segs + 1
2550
2551                    for i in range(1, 2 * d.gutter_segs):
2552                        j = i + f
2553                        faces.append((j, j + df, j + df + 1, j + 1))
2554                        uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)])
2555                        matids.append(idmat)
2556
2557                    """
2558                            segs = 6
2559
2560                            n_faces = segs / 2 - 1
2561
2562                                0           6
2563                                 1         5
2564                                   2     4
2565                                      3
2566                    """
2567                    # close start
2568                    if s1.type == 'SIDE':
2569
2570                        if d.gutter_segs % 2 == 0:
2571                            faces.append((f + n_faces + 3, f + n_faces + 1, f + n_faces + 2))
2572                            uvs.append([(0, 0), (1, 0), (0.5, -0.5)])
2573                            matids.append(idmat)
2574
2575                        for i in range(n_faces):
2576
2577                            j = i + f + 1
2578                            k = f + d.gutter_segs - i
2579                            faces.append((j + 1, k, k + 1, j))
2580                            uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)])
2581                            matids.append(idmat)
2582
2583                    # close end
2584                    if s2.type == 'SIDE':
2585
2586                        f += 2 * d.gutter_segs + 1
2587
2588                        if d.gutter_segs % 2 == 0:
2589                            faces.append((f + n_faces + 1, f + n_faces + 3, f + n_faces + 2))
2590                            uvs.append([(0, 0), (1, 0), (0.5, -0.5)])
2591                            matids.append(idmat)
2592
2593                        for i in range(n_faces):
2594
2595                            j = i + f + 1
2596                            k = f + d.gutter_segs - i
2597                            faces.append((j, k + 1, k, j + 1))
2598                            uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)])
2599                            matids.append(idmat)
2600
2601    def beam_primary(self, d, verts, faces, edges, matids, uvs):
2602
2603        idmat = 3
2604
2605        for pan in self.pans:
2606            for i, s in enumerate(pan.segs):
2607
2608                if s.type == 'AXIS':
2609
2610                    ####################
2611                    # Poutre Faitiere
2612                    ####################
2613
2614                    """
2615                     1___________________2   left
2616                    0|___________________|3  axis
2617                     |___________________|   right
2618                     5                   4
2619                    """
2620                    f = len(verts)
2621
2622                    s2 = s.offset(-0.5 * d.beam_width)
2623
2624                    # offset from roof border
2625                    s0 = pan.last_seg(i)
2626                    s1 = pan.next_seg(i)
2627                    t0 = 0
2628                    t1 = 1
2629
2630                    s0_tri = pan.next_tri
2631                    s1_tri = pan.node_tri
2632
2633                    if pan.side == 'LEFT':
2634                        s0_tri, s1_tri = s1_tri, s0_tri
2635
2636                    if s0.type == 'SIDE' and s.length > 0:
2637                        s0 = s0.offset(d.beam_offset)
2638                        t0 = -d.beam_offset / s.length
2639
2640                    if s0_tri:
2641                        p0 = s2.p0
2642                        t0 = 0
2643                    else:
2644                        res, p0, t = s2.intersect(s0)
2645                        if not res:
2646                            continue
2647
2648                    if s1.type == 'SIDE' and s.length > 0:
2649                        s1 = s1.offset(d.beam_offset)
2650                        t1 = 1 + d.beam_offset / s.length
2651
2652                    if s1_tri:
2653                        t1 = 1
2654                        p1 = s2.p1
2655                    else:
2656                        res, p1, t = s2.intersect(s1)
2657                        if not res:
2658                            continue
2659
2660                    x0, y0 = p0
2661                    x1, y1 = s.lerp(t0)
2662                    x2, y2 = p1
2663                    x3, y3 = s.lerp(t1)
2664                    z0 = self.z + d.beam_alt + pan.altitude(p0)
2665                    z1 = z0 - d.beam_height
2666                    z2 = self.z + d.beam_alt + pan.altitude(p1)
2667                    z3 = z2 - d.beam_height
2668                    verts.extend([
2669                        (x0, y0, z0),
2670                        (x1, y1, z0),
2671                        (x2, y2, z2),
2672                        (x3, y3, z2),
2673                        (x0, y0, z1),
2674                        (x1, y1, z1),
2675                        (x2, y2, z3),
2676                        (x3, y3, z3),
2677                    ])
2678                    if s0_tri or s0.type == 'SIDE':
2679                        faces.append((f + 4, f + 5, f + 1, f))
2680                        uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)])
2681                        matids.append(idmat)
2682                    if s1_tri or s1.type == 'SIDE':
2683                        faces.append((f + 2, f + 3, f + 7, f + 6))
2684                        uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)])
2685                        matids.append(idmat)
2686
2687                    faces.extend([
2688                        # internal side
2689                        # (f + 1, f + 5, f + 7, f + 3),
2690                        # external side
2691                        (f + 2, f + 6, f + 4, f),
2692                        # top
2693                        (f, f + 1, f + 3, f + 2),
2694                        # bottom
2695                        (f + 5, f + 4, f + 6, f + 7)
2696                    ])
2697                    matids.extend([
2698                        idmat, idmat, idmat
2699                    ])
2700                    uvs.extend([
2701                        [(0, 0), (0, 1), (1, 1), (1, 0)],
2702                        [(0, 0), (0, 1), (1, 1), (1, 0)],
2703                        [(0, 0), (0, 1), (1, 1), (1, 0)]
2704                    ])
2705
2706    def rafter(self, context, o, d):
2707
2708        idmat = 4
2709
2710        # Rafters / Chevrons
2711        start = max(0.001 + 0.5 * d.rafter_width, d.rafter_start)
2712
2713        holes_offset = -d.rafter_width
2714
2715        # build temp bmesh and bissect
2716        for pan in self.pans:
2717            tmin, tmax, ysize = pan.tmin, pan.tmax, pan.ysize
2718
2719            # print("tmin:%s tmax:%s ysize:%s" % (tmin, tmax, ysize))
2720
2721            f = 0
2722
2723            verts = []
2724            faces = []
2725            matids = []
2726            uvs = []
2727            alt = d.rafter_alt
2728            seg = pan.fake_axis
2729
2730            t0 = tmin + (start - 0.5 * d.rafter_width) / seg.length
2731            t1 = tmin + (start + 0.5 * d.rafter_width) / seg.length
2732
2733            tx = start / seg.length
2734            dt = d.rafter_spacing / seg.length
2735
2736            n_items = max(1, round((tmax - tmin) / dt, 0))
2737
2738            dt = ((tmax - tmin) - 2 * tx) / n_items
2739
2740            for j in range(int(n_items) + 1):
2741                n0 = seg.sized_normal(t1 + j * dt, - ysize)
2742                n1 = seg.sized_normal(t0 + j * dt, - ysize)
2743                f = len(verts)
2744
2745                z0 = self.z + alt + pan.altitude(n0.p0)
2746                x0, y0 = n0.p0
2747                z1 = self.z + alt + pan.altitude(n0.p1)
2748                x1, y1 = n0.p1
2749                z2 = self.z + alt + pan.altitude(n1.p0)
2750                x2, y2 = n1.p0
2751                z3 = self.z + alt + pan.altitude(n1.p1)
2752                x3, y3 = n1.p1
2753
2754                verts.extend([
2755                    (x0, y0, z0),
2756                    (x1, y1, z1),
2757                    (x2, y2, z2),
2758                    (x3, y3, z3)
2759                    ])
2760
2761                faces.append((f + 1, f, f + 2, f + 3))
2762                matids.append(idmat)
2763                uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)])
2764
2765            bm = bmed.buildmesh(
2766                context, o, verts, faces, matids=matids, uvs=uvs,
2767                weld=False, clean=False, auto_smooth=True, temporary=True)
2768
2769            self.cut_boundary(bm, pan)
2770            self.cut_holes(bm, pan, offset={'DEFAULT': holes_offset})
2771
2772            bmesh.ops.dissolve_limit(bm,
2773                        angle_limit=0.01,
2774                        use_dissolve_boundaries=False,
2775                        verts=bm.verts,
2776                        edges=bm.edges,
2777                        delimit={'MATERIAL'})
2778
2779            geom = bm.faces[:]
2780            verts = bm.verts[:]
2781            bmesh.ops.solidify(bm, geom=geom, thickness=0.0001)
2782            bmesh.ops.translate(bm, vec=Vector((0, 0, -d.rafter_height)), space=o.matrix_world, verts=verts)
2783            # uvs for sides
2784            uvs = [(0, 0), (1, 0), (1, 1), (0, 1)]
2785            layer = bm.loops.layers.uv.verify()
2786            for i, face in enumerate(bm.faces):
2787                if len(face.loops) == 4:
2788                    for j, loop in enumerate(face.loops):
2789                        loop[layer].uv = uvs[j]
2790
2791            # merge with object
2792            bmed.bmesh_join(context, o, [bm], normal_update=True)
2793
2794        bpy.ops.object.mode_set(mode='OBJECT')
2795
2796    def hips(self, d, verts, faces, edges, matids, uvs):
2797
2798        idmat_valley = 5
2799        idmat = 6
2800        idmat_poutre = 4
2801
2802        sx, sy, sz = d.hip_size_x, d.hip_size_y, d.hip_size_z
2803
2804        if d.hip_model == 'ROUND':
2805
2806            # round hips
2807            t_pts = [Vector((sx * x, sy * y, sz * z)) for x, y, z in [
2808                (-0.5, 0.34, 0.08), (-0.5, 0.32, 0.19), (0.5, -0.4, -0.5),
2809                (0.5, 0.4, -0.5), (-0.5, 0.26, 0.28), (-0.5, 0.16, 0.34),
2810                (-0.5, 0.05, 0.37), (-0.5, -0.05, 0.37), (-0.5, -0.16, 0.34),
2811                (-0.5, -0.26, 0.28), (-0.5, -0.32, 0.19), (-0.5, -0.34, 0.08),
2812                (-0.5, -0.25, -0.5), (-0.5, 0.25, -0.5), (0.5, -0.08, 0.5),
2813                (0.5, -0.5, 0.08), (0.5, -0.24, 0.47), (0.5, -0.38, 0.38),
2814                (0.5, -0.47, 0.24), (0.5, 0.5, 0.08), (0.5, 0.08, 0.5),
2815                (0.5, 0.47, 0.24), (0.5, 0.38, 0.38), (0.5, 0.24, 0.47)
2816            ]]
2817            t_faces = [
2818                (23, 22, 4, 5), (3, 19, 21, 22, 23, 20, 14, 16, 17, 18, 15, 2), (14, 20, 6, 7),
2819                (18, 17, 9, 10), (15, 18, 10, 11), (21, 19, 0, 1), (17, 16, 8, 9),
2820                (13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 1, 0), (19, 3, 13, 0), (20, 23, 5, 6), (22, 21, 1, 4),
2821                (3, 2, 12, 13), (2, 15, 11, 12), (16, 14, 7, 8)
2822            ]
2823            t_uvs = [
2824                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
2825                [(0.5, 1.0), (0.75, 0.93), (0.93, 0.75),
2826                (1.0, 0.5), (0.93, 0.25), (0.75, 0.07),
2827                (0.5, 0.0), (0.25, 0.07), (0.07, 0.25),
2828                (0.0, 0.5), (0.07, 0.75), (0.25, 0.93)],
2829                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
2830                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
2831                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
2832                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
2833                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
2834                [(0.5, 1.0), (0.75, 0.93), (0.93, 0.75),
2835                (1.0, 0.5), (0.93, 0.25), (0.75, 0.07),
2836                (0.5, 0.0), (0.25, 0.07), (0.07, 0.25),
2837                (0.0, 0.5), (0.07, 0.75), (0.25, 0.93)],
2838                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
2839                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
2840                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
2841                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
2842                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
2843                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]
2844            ]
2845            # affect vertex with slope
2846            t_left = []
2847            t_right = []
2848
2849        elif d.hip_model == 'ETERNIT':
2850
2851            # square hips "eternit like"
2852            t_pts = [Vector((sx * x, sy * y, sz * z)) for x, y, z in [
2853                (0.5, 0.5, 0.0), (-0.5, 0.5, -0.5), (0.5, -0.5, 0.0),
2854                (-0.5, -0.5, -0.5), (0.5, 0.0, 0.0), (-0.5, -0.0, -0.5),
2855                (0.5, 0.0, 0.5), (0.5, -0.5, 0.5), (-0.5, -0.5, 0.0),
2856                (-0.5, -0.0, 0.0), (0.5, 0.5, 0.5), (-0.5, 0.5, 0.0)]
2857            ]
2858            t_faces = [
2859                (4, 2, 3, 5), (0, 4, 5, 1), (6, 9, 8, 7),
2860                (10, 11, 9, 6), (0, 10, 6, 4), (5, 9, 11, 1),
2861                (2, 7, 8, 3), (1, 11, 10, 0), (4, 6, 7, 2), (3, 8, 9, 5)
2862                ]
2863            t_uvs = [
2864                [(0.0, 0.5), (0.0, 1.0), (1.0, 1.0), (1.0, 0.5)], [(0.0, 0.0), (0.0, 0.5), (1.0, 0.5), (1.0, 0.0)],
2865                [(0.0, 0.5), (1.0, 0.5), (1.0, 1.0), (0.0, 1.0)], [(0.0, 0.0), (1.0, 0.0), (1.0, 0.5), (0.0, 0.5)],
2866                [(0.0, 0.5), (0.0, 1.0), (0.5, 1.0), (0.5, 0.5)], [(0.5, 0.5), (0.5, 1.0), (0.0, 1.0), (0.0, 0.5)],
2867                [(0.0, 0.5), (0.0, 1.0), (1.0, 1.0), (1.0, 0.5)], [(0.0, 0.5), (0.0, 1.0), (-1.0, 1.0), (-1.0, 0.5)],
2868                [(0.5, 0.5), (0.5, 1.0), (1.0, 1.0), (1.0, 0.5)], [(0.0, 0.5), (0.0, 1.0), (-0.5, 1.0), (-0.5, 0.5)]
2869            ]
2870            t_left = [2, 3, 7, 8]
2871            t_right = [0, 1, 10, 11]
2872
2873        elif d.hip_model == 'FLAT':
2874            # square hips "eternit like"
2875            t_pts = [Vector((sx * x, sy * y, sz * z)) for x, y, z in [
2876                (-0.5, -0.4, 0.0), (-0.5, -0.4, 0.5), (-0.5, 0.4, 0.0),
2877                (-0.5, 0.4, 0.5), (0.5, -0.5, 0.5), (0.5, -0.5, 1.0),
2878                (0.5, 0.5, 0.5), (0.5, 0.5, 1.0), (-0.5, 0.33, 0.0),
2879                (-0.5, -0.33, 0.0), (0.5, -0.33, 0.5), (0.5, 0.33, 0.5),
2880                (-0.5, 0.33, -0.5), (-0.5, -0.33, -0.5), (0.5, -0.33, -0.5),
2881                (0.5, 0.33, -0.5)]
2882            ]
2883            t_faces = [
2884                (0, 1, 3, 2, 8, 9), (2, 3, 7, 6), (6, 7, 5, 4, 10, 11),
2885                (4, 5, 1, 0), (9, 10, 4, 0), (7, 3, 1, 5),
2886                (2, 6, 11, 8), (9, 8, 12, 13), (12, 15, 14, 13),
2887                (8, 11, 15, 12), (10, 9, 13, 14), (11, 10, 14, 15)]
2888            t_uvs = [
2889                [(0.5, 1.0), (0.93, 0.75), (0.93, 0.25), (0.5, 0.0), (0.07, 0.25), (0.07, 0.75)],
2890                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
2891                [(0.5, 1.0), (0.93, 0.75), (0.93, 0.25), (0.5, 0.0), (0.07, 0.25), (0.07, 0.75)],
2892                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
2893                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
2894                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
2895                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
2896                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
2897                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
2898                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
2899                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
2900                [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]
2901            ]
2902            t_left = []
2903            t_right = []
2904
2905        t_idmats = [idmat for f in t_faces]
2906
2907        for pan in self.pans:
2908            for i, s in enumerate(pan.segs):
2909                if ('LINK' in s.type and
2910                        d.beam_sec_enable):
2911                    ##############
2912                    # beam inside
2913                    ##############
2914                    f = len(verts)
2915
2916                    s0 = s.offset(-0.5 * d.beam_sec_width)
2917
2918                    s2 = pan.last_seg(i)
2919                    s3 = pan.next_seg(i)
2920                    p0 = s0.p0
2921                    p1 = s0.p1
2922                    t0 = 0
2923                    t1 = 1
2924                    res, p, t = s0.intersect(s2)
2925                    if res:
2926                        t0 = t
2927                        p0 = p
2928                    res, p, t = s0.intersect(s3)
2929                    if res:
2930                        t1 = t
2931                        p1 = p
2932
2933                    p0 = s.lerp(t0)
2934                    p1 = s.lerp(t1)
2935
2936                    x0, y0 = s0.lerp(t0)
2937                    x1, y1 = s.p0
2938
2939                    z0 = self.z + d.beam_sec_alt + pan.altitude(p0)
2940                    z1 = z0 - d.beam_sec_height
2941                    z2 = self.z + d.beam_sec_alt + pan.altitude(s.p0)
2942                    z3 = z2 - d.beam_sec_height
2943
2944                    verts.extend([
2945                        (x0, y0, z0),
2946                        (x0, y0, z1),
2947                        (x1, y1, z2),
2948                        (x1, y1, z3)
2949                    ])
2950
2951                    x2, y2 = s0.lerp(t1)
2952                    x3, y3 = s.p1
2953
2954                    z0 = self.z + d.beam_sec_alt + pan.altitude(p1)
2955                    z1 = z0 - d.beam_sec_height
2956                    z2 = self.z + d.beam_sec_alt + pan.altitude(s.p1)
2957                    z3 = z2 - d.beam_sec_height
2958
2959                    verts.extend([
2960                        (x2, y2, z0),
2961                        (x2, y2, z1),
2962                        (x3, y3, z2),
2963                        (x3, y3, z3)
2964                    ])
2965
2966                    faces.extend([
2967                        (f, f + 4, f + 5, f + 1),
2968                        (f + 1, f + 5, f + 7, f + 3),
2969                        (f + 2, f + 3, f + 7, f + 6),
2970                        (f + 2, f + 6, f + 4, f),
2971                        (f, f + 1, f + 3, f + 2),
2972                        (f + 5, f + 4, f + 6, f + 7)
2973                    ])
2974                    matids.extend([
2975                        idmat_poutre, idmat_poutre, idmat_poutre,
2976                        idmat_poutre, idmat_poutre, idmat_poutre
2977                    ])
2978                    uvs.extend([
2979                        [(0, 0), (1, 0), (1, 1), (0, 1)],
2980                        [(0, 0), (1, 0), (1, 1), (0, 1)],
2981                        [(0, 0), (1, 0), (1, 1), (0, 1)],
2982                        [(0, 0), (1, 0), (1, 1), (0, 1)],
2983                        [(0, 0), (1, 0), (1, 1), (0, 1)],
2984                        [(0, 0), (1, 0), (1, 1), (0, 1)]
2985                    ])
2986
2987                if s.type == 'LINK_HIP':
2988
2989                    # TODO:
2990                    # Slice borders properly
2991
2992                    if d.hip_enable:
2993
2994                        s0 = pan.last_seg(i)
2995                        s1 = pan.next_seg(i)
2996                        s2 = s
2997                        p0 = s0.p1
2998                        p1 = s1.p0
2999                        z0 = pan.altitude(p0)
3000                        z1 = pan.altitude(p1)
3001
3002                        # s0 is top seg
3003                        if z1 > z0:
3004                            p0, p1 = p1, p0
3005                            z0, z1 = z1, z0
3006                            s2 = s2.oposite
3007                        dz = pan.altitude(s2.sized_normal(0, 1).p1) - z0
3008
3009                        if dz < 0:
3010                            s1 = s1.offset(d.tile_border)
3011                            # vx from p0 to p1
3012                            x, y = p1 - p0
3013                            v = Vector((x, y, z1 - z0))
3014                            vx = v.normalized()
3015                            vy = vx.cross(Vector((0, 0, 1)))
3016                            vz = vy.cross(vx)
3017
3018                            x0, y0 = p0 + d.hip_alt * vz.to_2d()
3019                            z2 = z0 + self.z + d.hip_alt * vz.z
3020                            tM = Matrix([
3021                                [vx.x, vy.x, vz.x, x0],
3022                                [vx.y, vy.y, vz.y, y0],
3023                                [vx.z, vy.z, vz.z, z2],
3024                                [0, 0, 0, 1]
3025                            ])
3026                            space_x = v.length - d.tile_border
3027                            n_x = 1 + int(space_x / d.hip_space_x)
3028                            dx = space_x / n_x
3029                            x0 = 0.5 * dx
3030
3031                            t_verts = [p for p in t_pts]
3032
3033                            # apply slope
3034
3035                            for i in t_left:
3036                                t_verts[i] = t_verts[i].copy()
3037                                t_verts[i].z -= dz * t_verts[i].y
3038                            for i in t_right:
3039                                t_verts[i] = t_verts[i].copy()
3040                                t_verts[i].z += dz * t_verts[i].y
3041
3042                            for k in range(n_x):
3043                                lM = tM @ Matrix([
3044                                    [1, 0, 0, x0 + k * dx],
3045                                    [0, -1, 0, 0],
3046                                    [0, 0, 1, 0],
3047                                    [0, 0, 0, 1]
3048                                ])
3049                                f = len(verts)
3050
3051                                verts.extend([lM @ p for p in t_verts])
3052                                faces.extend([tuple(i + f for i in p) for p in t_faces])
3053                                matids.extend(t_idmats)
3054                                uvs.extend(t_uvs)
3055
3056                elif s.type == 'LINK_VALLEY':
3057                    if d.valley_enable:
3058                        f = len(verts)
3059                        s0 = s.offset(-2 * d.tile_couloir)
3060                        s1 = pan.last_seg(i)
3061                        s2 = pan.next_seg(i)
3062                        p0 = s0.p0
3063                        p1 = s0.p1
3064                        res, p, t = s0.intersect(s1)
3065                        if res:
3066                            p0 = p
3067                        res, p, t = s0.intersect(s2)
3068                        if res:
3069                            p1 = p
3070                        alt = self.z + d.valley_altitude
3071                        x0, y0 = s1.p1
3072                        x1, y1 = p0
3073                        x2, y2 = p1
3074                        x3, y3 = s2.p0
3075                        z0 = alt + pan.altitude(s1.p1)
3076                        z1 = alt + pan.altitude(p0)
3077                        z2 = alt + pan.altitude(p1)
3078                        z3 = alt + pan.altitude(s2.p0)
3079
3080                        verts.extend([
3081                            (x0, y0, z0),
3082                            (x1, y1, z1),
3083                            (x2, y2, z2),
3084                            (x3, y3, z3),
3085                        ])
3086                        faces.extend([
3087                            (f, f + 3, f + 2, f + 1)
3088                        ])
3089                        matids.extend([
3090                            idmat_valley
3091                            ])
3092                        uvs.extend([
3093                            [(0, 0), (1, 0), (1, 1), (0, 1)]
3094                        ])
3095
3096                elif s.type == 'AXIS' and d.hip_enable and pan.side == 'LEFT':
3097
3098                    tmin = 0
3099                    tmax = 1
3100                    s0 = pan.last_seg(i)
3101                    if s0.type == 'SIDE' and s.length > 0:
3102                        tmin = 0 - d.tile_side / s.length
3103                    s1 = pan.next_seg(i)
3104
3105                    if s1.type == 'SIDE' and s.length > 0:
3106                        tmax = 1 + d.tile_side / s.length
3107
3108                    # print("tmin:%s tmax:%s" % (tmin, tmax))
3109                    ####################
3110                    # Faitiere
3111                    ####################
3112
3113                    f = len(verts)
3114                    s_len = (tmax - tmin) * s.length
3115                    n_obj = 1 + int(s_len / d.hip_space_x)
3116                    dx = s_len / n_obj
3117                    x0 = 0.5 * dx
3118                    v = s.v.normalized()
3119                    p0 = s.lerp(tmin)
3120                    tM = Matrix([
3121                        [v.x, v.y, 0, p0.x],
3122                        [v.y, -v.x, 0, p0.y],
3123                        [0, 0, 1, self.z + d.hip_alt],
3124                        [0, 0, 0, 1]
3125                    ])
3126                    t_verts = [p.copy() for p in t_pts]
3127
3128                    # apply slope
3129                    for i in t_left:
3130                        t_verts[i].z += t_verts[i].y * (pan.other_side.slope - d.tile_size_z / d.tile_size_y)
3131                    for i in t_right:
3132                        t_verts[i].z -= t_verts[i].y * (pan.slope - d.tile_size_z / d.tile_size_y)
3133
3134                    for k in range(n_obj):
3135                        lM = tM @ Matrix([
3136                            [1, 0, 0, x0 + k * dx],
3137                            [0, -1, 0, 0],
3138                            [0, 0, 1, 0],
3139                            [0, 0, 0, 1]
3140                        ])
3141                        v = len(verts)
3142                        verts.extend([lM @ p for p in t_verts])
3143                        faces.extend([tuple(i + v for i in f) for f in t_faces])
3144                        matids.extend(t_idmats)
3145                        uvs.extend(t_uvs)
3146
3147    def make_hole(self, context, hole_obj, o, d, update_parent=False):
3148        """
3149            Hole for t child on parent
3150            create / update a RoofCutter on parent
3151            assume context object is child roof
3152            with parent set
3153        """
3154        # print("Make hole :%s hole_obj:%s" % (o.name, hole_obj))
3155        if o.parent is None:
3156            return
3157        # root is a RoofSegment
3158        root = self.nodes[0].root
3159        r_pan = root.right
3160        l_pan = root.left
3161
3162        # merge :
3163        # 5   ____________ 4
3164        #    /            |
3165        #   /     left    |
3166        #  /_____axis_____|  3 <- kill axis and this one
3167        # 0\              |
3168        #   \     right   |
3169        # 1  \____________| 2
3170        #
3171        # degenerate case:
3172        #
3173        #  /|
3174        # / |
3175        # \ |
3176        #  \|
3177        #
3178
3179        segs = []
3180        last = len(r_pan.segs) - 1
3181        for i, seg in enumerate(r_pan.segs):
3182            # r_pan start parent roof side
3183            if i == last:
3184                to_merge = seg.copy
3185            elif seg.type != 'AXIS':
3186                segs.append(seg.copy)
3187
3188        for i, seg in enumerate(l_pan.segs):
3189            # l_pan end parent roof side
3190            if i == 1:
3191                # 0 is axis
3192                to_merge.p1 = seg.p1
3193                segs.append(to_merge)
3194            elif seg.type != 'AXIS':
3195                segs.append(seg.copy)
3196
3197        # if there is side offset:
3198        # create an arrow
3199        #
3200        # 4   s4
3201        #    /|
3202        #   / |___s1_______
3203        #  / p3            | p2  s3
3204        # 0\ p0___s0_______| p1
3205        #   \ |
3206        # 1  \|
3207        s0 = root.left._axis.offset(
3208                max(0.001,
3209                    min(
3210                        root.right.ysize - 0.001,
3211                        root.right.ysize - d.hole_offset_right
3212                        )
3213                    ))
3214        s1 = root.left._axis.offset(
3215                -max(0.001,
3216                    min(
3217                        root.left.ysize - 0.001,
3218                        root.left.ysize - d.hole_offset_left
3219                        )
3220                    ))
3221
3222        s3 = segs[2].offset(
3223            -min(root.left.xsize - 0.001, d.hole_offset_front)
3224            )
3225        s4 = segs[0].copy
3226        p1 = s4.p1
3227        s4.p1 = segs[-1].p0
3228        s4.p0 = p1
3229        res, p0, t = s4.intersect(s0)
3230        res, p1, t = s0.intersect(s3)
3231        res, p2, t = s1.intersect(s3)
3232        res, p3, t = s4.intersect(s1)
3233        pts = []
3234        # pts in cw order for 'DIFFERENCE' mode
3235        pts.extend([segs[-1].p1, segs[-1].p0])
3236        if (segs[-1].p0 - p3).length > 0.001:
3237            pts.append(p3)
3238        pts.extend([p2, p1])
3239        if (segs[0].p1 - p0).length > 0.001:
3240            pts.append(p0)
3241        pts.extend([segs[0].p1, segs[0].p0])
3242
3243        pts = [p.to_3d() for p in pts]
3244
3245        if hole_obj is None:
3246            context.view_layer.objects.active = o.parent
3247            bpy.ops.archipack.roof_cutter(parent=d.t_parent, auto_manipulate=False)
3248            hole_obj = context.active_object
3249        else:
3250            context.view_layer.objects.active = hole_obj
3251
3252        hole_obj.select_set(state=True)
3253        if d.parts[0].a0 < 0:
3254            y = -d.t_dist_y
3255        else:
3256            y = d.t_dist_y
3257
3258        hole_obj.matrix_world = o.matrix_world @ Matrix([
3259            [1, 0, 0, 0],
3260            [0, 1, 0, y],
3261            [0, 0, 1, 0],
3262            [0, 0, 0, 1]
3263            ])
3264
3265        hd = archipack_roof_cutter.datablock(hole_obj)
3266        hd.boundary = o.name
3267        hd.update_points(context, hole_obj, pts, update_parent=update_parent)
3268        hole_obj.select_set(state=False)
3269
3270        context.view_layer.objects.active = o
3271
3272    def change_coordsys(self, fromTM, toTM):
3273        """
3274            move shape fromTM into toTM coordsys
3275        """
3276        dp = (toTM.inverted() @ fromTM.translation).to_2d()
3277        da = toTM.row[1].to_2d().angle_signed(fromTM.row[1].to_2d())
3278        ca = cos(da)
3279        sa = sin(da)
3280        rM = Matrix([
3281            [ca, -sa],
3282            [sa, ca]
3283            ])
3284        for s in self.segs:
3285            tp = (rM @ s.p0) - s.p0 + dp
3286            s.rotate(da)
3287            s.translate(tp)
3288
3289    def t_partition(self, array, begin, end):
3290        pivot = begin
3291        for i in range(begin + 1, end + 1):
3292            # wall idx
3293            if array[i][0] < array[begin][0]:
3294                pivot += 1
3295                array[i], array[pivot] = array[pivot], array[i]
3296        array[pivot], array[begin] = array[begin], array[pivot]
3297        return pivot
3298
3299    def sort_t(self, array, begin=0, end=None):
3300        # print("sort_child")
3301        if end is None:
3302            end = len(array) - 1
3303
3304        def _quicksort(array, begin, end):
3305            if begin >= end:
3306                return
3307            pivot = self.t_partition(array, begin, end)
3308            _quicksort(array, begin, pivot - 1)
3309            _quicksort(array, pivot + 1, end)
3310        return _quicksort(array, begin, end)
3311
3312    def make_wall_fit(self, context, o, wall, inside):
3313        wd = wall.data.archipack_wall2[0]
3314        wg = wd.get_generator()
3315        z0 = self.z - wd.z
3316
3317        # wg in roof coordsys
3318        wg.change_coordsys(wall.matrix_world, o.matrix_world)
3319
3320        if inside:
3321            # fit inside
3322            offset = -0.5 * (1 - wd.x_offset) * wd.width
3323        else:
3324            # fit outside
3325            offset = 0
3326
3327        wg.set_offset(offset)
3328
3329        wall_t = [[] for w in wg.segs]
3330
3331        for pan in self.pans:
3332            # walls segment
3333            for widx, wseg in enumerate(wg.segs):
3334
3335                ls = wseg.line.length
3336
3337                for seg in pan.segs:
3338                    # intersect with a roof segment
3339                    # any linked or axis intersection here
3340                    # will be dup as they are between 2 roof parts
3341                    res, p, t, v = wseg.line.intersect_ext(seg)
3342                    if res:
3343                        z = z0 + pan.altitude(p)
3344                        wall_t[widx].append((t, z, t * ls))
3345
3346                # lie under roof
3347                if type(wseg).__name__ == "CurvedWall":
3348                    for step in range(12):
3349                        t = step / 12
3350                        p = wseg.line.lerp(t)
3351                        if pan.inside(p):
3352                            z = z0 + pan.altitude(p)
3353                            wall_t[widx].append((t, z, t * ls))
3354                else:
3355                    if pan.inside(wseg.line.p0):
3356                        z = z0 + pan.altitude(wseg.line.p0)
3357                        wall_t[widx].append((0, z, 0))
3358
3359        old = context.active_object
3360        old_sel = wall.select_get()
3361        wall.select_set(state=True)
3362        context.view_layer.objects.active = wall
3363
3364        wd.auto_update = False
3365        # setup splits count and first split to 0
3366        for widx, seg in enumerate(wall_t):
3367            self.sort_t(seg)
3368            # print("seg: %s" % seg)
3369            for s in seg:
3370                t, z, d = s
3371                wd.parts[widx].n_splits = len(seg) + 1
3372                wd.parts[widx].z[0] = 0
3373                wd.parts[widx].t[0] = 0
3374                break
3375
3376        # add splits, skip dups
3377        for widx, seg in enumerate(wall_t):
3378            t0 = 0
3379            last_d = -1
3380            sid = 1
3381            for s in seg:
3382                t, z, d = s
3383                if t == 0:
3384                    # add at end of last segment
3385                    if widx > 0:
3386                        lid = wd.parts[widx - 1].n_splits - 1
3387                        wd.parts[widx - 1].z[lid] = z
3388                        wd.parts[widx - 1].t[lid] = 1
3389                    else:
3390                        wd.parts[widx].z[0] = z
3391                        wd.parts[widx].t[0] = t
3392                        sid = 1
3393                else:
3394                    if d - last_d < 0.001:
3395                        wd.parts[widx].n_splits -= 1
3396                        continue
3397                    wd.parts[widx].z[sid] = z
3398                    wd.parts[widx].t[sid] = t - t0
3399                    t0 = t
3400                    sid += 1
3401                    last_d = d
3402
3403        if wd.closed:
3404            last = wd.parts[wd.n_parts].n_splits - 1
3405            wd.parts[wd.n_parts].z[last] = wd.parts[0].z[0]
3406            wd.parts[wd.n_parts].t[last] = 1.0
3407
3408        wd.auto_update = True
3409        """
3410        for s in self.segs:
3411            s.as_curve(context)
3412        for s in wg.segs:
3413            s.as_curve(context)
3414        """
3415        wall.select_set(state=old_sel)
3416        context.view_layer.objects.active = old
3417
3418    def boundary(self, context, o):
3419        """
3420            either external or holes cuts
3421        """
3422        to_remove = []
3423        for b in o.children:
3424            d = archipack_roof_cutter.datablock(b)
3425            if d is not None:
3426                g = d.ensure_direction()
3427                g.change_coordsys(b.matrix_world, o.matrix_world)
3428                for i, pan in enumerate(self.pans):
3429                    keep = pan.slice(g)
3430                    if not keep:
3431                        if i not in to_remove:
3432                            to_remove.append(i)
3433                    pan.limits()
3434        to_remove.sort()
3435        for i in reversed(to_remove):
3436            self.pans.pop(i)
3437
3438    def draft(self, context, verts, edges):
3439        for pan in self.pans:
3440            pan.draw(context, self.z, verts, edges)
3441
3442        for s in self.segs:
3443            if s.constraint_type == 'SLOPE':
3444                f = len(verts)
3445                p0 = s.p0.to_3d()
3446                p0.z = self.z
3447                p1 = s.p1.to_3d()
3448                p1.z = self.z
3449                verts.extend([p0, p1])
3450                edges.append([f, f + 1])
3451
3452
3453def update(self, context):
3454    self.update(context)
3455
3456
3457def update_manipulators(self, context):
3458    self.update(context, manipulable_refresh=True)
3459
3460
3461def update_path(self, context):
3462    self.update_path(context)
3463
3464
3465def update_parent(self, context):
3466
3467    # update part a0
3468    o = context.active_object
3469    p, d = self.find_parent(context)
3470
3471    if d is not None:
3472
3473        o.parent = p
3474
3475        # trigger object update
3476        # hole creation and parent's update
3477
3478        self.parts[0].a0 = pi / 2
3479
3480    elif self.t_parent != "":
3481        self.t_parent = ""
3482
3483
3484def update_cutter(self, context):
3485    self.update(context, update_hole=True)
3486
3487
3488def update_childs(self, context):
3489    self.update(context, update_childs=True, update_hole=True)
3490
3491
3492def update_components(self, context):
3493    self.update(context, update_parent=False, update_hole=False)
3494
3495
3496class ArchipackSegment():
3497    length : FloatProperty(
3498            name="Length",
3499            min=0.01,
3500            max=1000.0,
3501            default=4.0,
3502            update=update
3503            )
3504    a0 : FloatProperty(
3505            name="Angle",
3506            min=-2 * pi,
3507            max=2 * pi,
3508            default=0,
3509            subtype='ANGLE', unit='ROTATION',
3510            update=update_cutter
3511            )
3512    manipulators : CollectionProperty(type=archipack_manipulator)
3513
3514
3515class ArchipackLines():
3516    n_parts : IntProperty(
3517            name="Parts",
3518            min=1,
3519            default=1, update=update_manipulators
3520            )
3521    # UI layout related
3522    parts_expand : BoolProperty(
3523            default=False
3524            )
3525
3526    def draw(self, layout, context):
3527        box = layout.box()
3528        row = box.row()
3529        if self.parts_expand:
3530            row.prop(self, 'parts_expand', icon="TRIA_DOWN", text="Parts", emboss=False)
3531            box.prop(self, 'n_parts')
3532            for i, part in enumerate(self.parts):
3533                part.draw(layout, context, i)
3534        else:
3535            row.prop(self, 'parts_expand', icon="TRIA_RIGHT", text="Parts", emboss=False)
3536
3537    def update_parts(self):
3538        # print("update_parts")
3539        # remove rows
3540        # NOTE:
3541        # n_parts+1
3542        # as last one is end point of last segment or closing one
3543        for i in range(len(self.parts), self.n_parts + 1, -1):
3544            self.parts.remove(i - 1)
3545
3546        # add rows
3547        for i in range(len(self.parts), self.n_parts + 1):
3548            self.parts.add()
3549
3550        self.setup_manipulators()
3551
3552    def setup_parts_manipulators(self):
3553        for i in range(self.n_parts + 1):
3554            p = self.parts[i]
3555            n_manips = len(p.manipulators)
3556            if n_manips < 1:
3557                s = p.manipulators.add()
3558                s.type_key = "ANGLE"
3559                s.prop1_name = "a0"
3560            if n_manips < 2:
3561                s = p.manipulators.add()
3562                s.type_key = "SIZE"
3563                s.prop1_name = "length"
3564            if n_manips < 3:
3565                s = p.manipulators.add()
3566                s.type_key = 'WALL_SNAP'
3567                s.prop1_name = str(i)
3568                s.prop2_name = 'z'
3569            if n_manips < 4:
3570                s = p.manipulators.add()
3571                s.type_key = 'DUMB_STRING'
3572                s.prop1_name = str(i + 1)
3573            if n_manips < 5:
3574                s = p.manipulators.add()
3575                s.type_key = "SIZE"
3576                s.prop1_name = "offset"
3577            p.manipulators[2].prop1_name = str(i)
3578            p.manipulators[3].prop1_name = str(i + 1)
3579
3580
3581class archipack_roof_segment(ArchipackSegment, PropertyGroup):
3582
3583    bound_idx : IntProperty(
3584            name="Link to",
3585            default=0,
3586            min=0,
3587            update=update_manipulators
3588            )
3589    width_left : FloatProperty(
3590            name="L Width",
3591            min=0.01,
3592            default=3.0,
3593            update=update_cutter
3594            )
3595    width_right : FloatProperty(
3596            name="R Width",
3597            min=0.01,
3598            default=3.0,
3599            update=update_cutter
3600            )
3601    slope_left : FloatProperty(
3602            name="L slope",
3603            min=0.0,
3604            default=0.3,
3605            update=update_cutter
3606            )
3607    slope_right : FloatProperty(
3608            name="R slope",
3609            min=0.0,
3610            default=0.3,
3611            update=update_cutter
3612            )
3613    auto_left : EnumProperty(
3614            description="Left mode",
3615            name="Left",
3616            items=(
3617                ('AUTO', 'Auto', '', 0),
3618                ('WIDTH', 'Width', '', 1),
3619                ('SLOPE', 'Slope', '', 2),
3620                ('ALL', 'All', '', 3),
3621                ),
3622            default="AUTO",
3623            update=update_manipulators
3624            )
3625    auto_right : EnumProperty(
3626            description="Right mode",
3627            name="Right",
3628            items=(
3629                ('AUTO', 'Auto', '', 0),
3630                ('WIDTH', 'Width', '', 1),
3631                ('SLOPE', 'Slope', '', 2),
3632                ('ALL', 'All', '', 3),
3633                ),
3634            default="AUTO",
3635            update=update_manipulators
3636            )
3637    triangular_end : BoolProperty(
3638            name="Triangular end",
3639            default=False,
3640            update=update
3641            )
3642    take_precedence : BoolProperty(
3643            name="Take precedence",
3644            description="On T segment take width precedence",
3645            default=False,
3646            update=update
3647            )
3648
3649    constraint_type : EnumProperty(
3650            items=(
3651                ('HORIZONTAL', 'Horizontal', '', 0),
3652                ('SLOPE', 'Slope', '', 1)
3653                ),
3654            default='HORIZONTAL',
3655            update=update_manipulators
3656            )
3657
3658    enforce_part : EnumProperty(
3659            name="Enforce part",
3660            items=(
3661                ('AUTO', 'Auto', '', 0),
3662                ('VALLEY', 'Valley', '', 1),
3663                ('HIP', 'Hip', '', 2)
3664                ),
3665            default='AUTO',
3666            update=update
3667            )
3668
3669    def find_in_selection(self, context):
3670        """
3671            find witch selected object this instance belongs to
3672            provide support for "copy to selected"
3673        """
3674        selected = context.selected_objects[:]
3675        for o in selected:
3676            d = archipack_roof.datablock(o)
3677            if d:
3678                for part in d.parts:
3679                    if part == self:
3680                        return d
3681        return None
3682
3683    def draw(self, layout, context, index):
3684        box = layout.box()
3685        if index > 0:
3686            box.prop(self, "constraint_type", text=str(index + 1))
3687            if self.constraint_type == 'SLOPE':
3688                box.prop(self, "enforce_part", text="")
3689        else:
3690            box.label(text="Part 1:")
3691        box.prop(self, "length")
3692        box.prop(self, "a0")
3693
3694        if index > 0:
3695            box.prop(self, 'bound_idx')
3696            if self.constraint_type == 'HORIZONTAL':
3697                box.prop(self, "triangular_end")
3698                row = box.row(align=True)
3699                row.prop(self, "auto_left", text="")
3700                row.prop(self, "auto_right", text="")
3701                if self.auto_left in {'ALL', 'WIDTH'}:
3702                    box.prop(self, "width_left")
3703                if self.auto_left in {'ALL', 'SLOPE'}:
3704                    box.prop(self, "slope_left")
3705                if self.auto_right in {'ALL', 'WIDTH'}:
3706                    box.prop(self, "width_right")
3707                if self.auto_right in {'ALL', 'SLOPE'}:
3708                    box.prop(self, "slope_right")
3709        elif self.constraint_type == 'HORIZONTAL':
3710            box.prop(self, "triangular_end")
3711
3712    def update(self, context, manipulable_refresh=False, update_hole=False):
3713        props = self.find_in_selection(context)
3714        if props is not None:
3715            props.update(context,
3716                manipulable_refresh,
3717                update_parent=True,
3718                update_hole=True,
3719                update_childs=True)
3720
3721
3722class archipack_roof(ArchipackLines, ArchipackObject, Manipulable, PropertyGroup):
3723    parts : CollectionProperty(type=archipack_roof_segment)
3724    z : FloatProperty(
3725            name="Altitude",
3726            default=3, precision=2, step=1,
3727            unit='LENGTH', subtype='DISTANCE',
3728            update=update_childs
3729            )
3730    slope_left : FloatProperty(
3731            name="L slope",
3732            default=0.5, precision=2, step=1,
3733            update=update_childs
3734            )
3735    slope_right : FloatProperty(
3736            name="R slope",
3737            default=0.5, precision=2, step=1,
3738            update=update_childs
3739            )
3740    width_left : FloatProperty(
3741            name="L width",
3742            default=3, precision=2, step=1,
3743            unit='LENGTH', subtype='DISTANCE',
3744            update=update_cutter
3745            )
3746    width_right : FloatProperty(
3747            name="R width",
3748            default=3, precision=2, step=1,
3749            unit='LENGTH', subtype='DISTANCE',
3750            update=update_cutter
3751            )
3752    draft : BoolProperty(
3753            options={'SKIP_SAVE'},
3754            name="Draft mode",
3755            default=False,
3756            update=update_manipulators
3757            )
3758    auto_update : BoolProperty(
3759            options={'SKIP_SAVE'},
3760            default=True,
3761            update=update_manipulators
3762            )
3763    quick_edit : BoolProperty(
3764            options={'SKIP_SAVE'},
3765            name="Quick Edit",
3766            default=False
3767            )
3768
3769    tile_enable : BoolProperty(
3770            name="Enable",
3771            default=True,
3772            update=update_components
3773            )
3774    tile_solidify : BoolProperty(
3775            name="Solidify",
3776            default=True,
3777            update=update_components
3778            )
3779    tile_height : FloatProperty(
3780            name="Height",
3781            description="Amount for solidify",
3782            min=0,
3783            default=0.02,
3784            unit='LENGTH', subtype='DISTANCE',
3785            update=update_components
3786            )
3787    tile_bevel : BoolProperty(
3788            name="Bevel",
3789            default=False,
3790            update=update_components
3791            )
3792    tile_bevel_amt : FloatProperty(
3793            name="Amount",
3794            description="Amount for bevel",
3795            min=0,
3796            default=0.02,
3797            unit='LENGTH', subtype='DISTANCE',
3798            update=update_components
3799            )
3800    tile_bevel_segs : IntProperty(
3801            name="Segs",
3802            description="Bevel Segs",
3803            min=1,
3804            default=2,
3805            update=update_components
3806            )
3807    tile_alternate : BoolProperty(
3808            name="Alternate",
3809            default=False,
3810            update=update_components
3811            )
3812    tile_offset : FloatProperty(
3813            name="Offset",
3814            description="Offset from start",
3815            min=0,
3816            max=100,
3817            subtype="PERCENTAGE",
3818            update=update_components
3819            )
3820    tile_altitude : FloatProperty(
3821            name="Altitude",
3822            description="Altitude from roof",
3823            default=0.1,
3824            unit='LENGTH', subtype='DISTANCE',
3825            update=update_components
3826            )
3827    tile_size_x : FloatProperty(
3828            name="Width",
3829            description="Size of tiles on x axis",
3830            min=0.01,
3831            default=0.2,
3832            unit='LENGTH', subtype='DISTANCE',
3833            update=update_components
3834            )
3835    tile_size_y : FloatProperty(
3836            name="Length",
3837            description="Size of tiles on y axis",
3838            min=0.01,
3839            default=0.3,
3840            unit='LENGTH', subtype='DISTANCE',
3841            update=update_components
3842            )
3843    tile_size_z : FloatProperty(
3844            name="Thickness",
3845            description="Size of tiles on z axis",
3846            min=0.0,
3847            default=0.02,
3848            unit='LENGTH', subtype='DISTANCE',
3849            update=update_components
3850            )
3851    tile_space_x : FloatProperty(
3852            name="Width",
3853            description="Space between tiles on x axis",
3854            min=0.01,
3855            default=0.2,
3856            unit='LENGTH', subtype='DISTANCE',
3857            update=update_components
3858            )
3859    tile_space_y : FloatProperty(
3860            name="Length",
3861            description="Space between tiles on y axis",
3862            min=0.01,
3863            default=0.3,
3864            unit='LENGTH', subtype='DISTANCE',
3865            update=update_components
3866            )
3867    tile_fit_x : BoolProperty(
3868            name="Fit x",
3869            description="Fit roof on x axis",
3870            default=True,
3871            update=update_components
3872            )
3873    tile_fit_y : BoolProperty(
3874            name="Fit y",
3875            description="Fit roof on y axis",
3876            default=True,
3877            update=update_components
3878            )
3879    tile_expand : BoolProperty(
3880            options={'SKIP_SAVE'},
3881            name="Tiles",
3882            description="Expand tiles panel",
3883            default=False
3884            )
3885    tile_model : EnumProperty(
3886            name="Model",
3887            items=(
3888                ('BRAAS1', 'Braas 1', '', 0),
3889                ('BRAAS2', 'Braas 2', '', 1),
3890                ('ETERNIT', 'Eternit', '', 2),
3891                ('LAUZE', 'Lauze', '', 3),
3892                ('ROMAN', 'Roman', '', 4),
3893                ('ROUND', 'Round', '', 5),
3894                ('PLACEHOLDER', 'Square', '', 6),
3895                ('ONDULEE', 'Ondule', '', 7),
3896                ('METAL', 'Metal', '', 8),
3897                # ('USER', 'User defined', '', 7)
3898                ),
3899            default="BRAAS2",
3900            update=update_components
3901            )
3902    tile_side : FloatProperty(
3903            name="Side",
3904            description="Space on side",
3905            default=0,
3906            unit='LENGTH', subtype='DISTANCE',
3907            update=update_components
3908            )
3909    tile_couloir : FloatProperty(
3910            name="Valley",
3911            description="Space between tiles on valley",
3912            min=0,
3913            default=0.05,
3914            unit='LENGTH', subtype='DISTANCE',
3915            update=update_components
3916            )
3917    tile_border : FloatProperty(
3918            name="Bottom",
3919            description="Tiles offset from bottom",
3920            default=0,
3921            unit='LENGTH', subtype='DISTANCE',
3922            update=update_components
3923            )
3924
3925    gutter_expand : BoolProperty(
3926            options={'SKIP_SAVE'},
3927            name="Gutter",
3928            description="Expand gutter panel",
3929            default=False
3930            )
3931    gutter_enable : BoolProperty(
3932            name="Enable",
3933            default=True,
3934            update=update_components
3935            )
3936    gutter_alt : FloatProperty(
3937            name="Altitude",
3938            description="altitude",
3939            default=0,
3940            unit='LENGTH', subtype='DISTANCE',
3941            update=update_components
3942            )
3943    gutter_width : FloatProperty(
3944            name="Width",
3945            description="Width",
3946            min=0.01,
3947            default=0.15,
3948            unit='LENGTH', subtype='DISTANCE',
3949            update=update_components
3950            )
3951    gutter_dist : FloatProperty(
3952            name="Spacing",
3953            description="Spacing",
3954            min=0,
3955            default=0.05,
3956            unit='LENGTH', subtype='DISTANCE',
3957            update=update_components
3958            )
3959    gutter_boudin : FloatProperty(
3960            name="Small width",
3961            description="Small width",
3962            min=0,
3963            default=0.015,
3964            unit='LENGTH', subtype='DISTANCE',
3965            update=update_components
3966            )
3967    gutter_segs : IntProperty(
3968            default=6,
3969            min=1,
3970            name="Segs",
3971            update=update_components
3972            )
3973
3974    beam_expand : BoolProperty(
3975            options={'SKIP_SAVE'},
3976            name="Beam",
3977            description="Expand beam panel",
3978            default=False
3979            )
3980    beam_enable : BoolProperty(
3981            name="Ridge pole",
3982            default=True,
3983            update=update_components
3984            )
3985    beam_width : FloatProperty(
3986            name="Width",
3987            description="Width",
3988            min=0.01,
3989            default=0.2,
3990            unit='LENGTH', subtype='DISTANCE',
3991            update=update_components
3992            )
3993    beam_height : FloatProperty(
3994            name="Height",
3995            description="Height",
3996            min=0.01,
3997            default=0.35,
3998            unit='LENGTH', subtype='DISTANCE',
3999            update=update_components
4000            )
4001    beam_offset : FloatProperty(
4002            name="Offset",
4003            description="Distance from roof border",
4004            default=0.02,
4005            unit='LENGTH', subtype='DISTANCE',
4006            update=update_components
4007            )
4008    beam_alt : FloatProperty(
4009            name="Altitude",
4010            description="Altitude from roof",
4011            default=-0.15,
4012            unit='LENGTH', subtype='DISTANCE',
4013            update=update_components
4014            )
4015    beam_sec_enable : BoolProperty(
4016            name="Hip rafter",
4017            default=True,
4018            update=update_components
4019            )
4020    beam_sec_width : FloatProperty(
4021            name="Width",
4022            description="Width",
4023            min=0.01,
4024            default=0.15,
4025            unit='LENGTH', subtype='DISTANCE',
4026            update=update_components
4027            )
4028    beam_sec_height : FloatProperty(
4029            name="Height",
4030            description="Height",
4031            min=0.01,
4032            default=0.2,
4033            unit='LENGTH', subtype='DISTANCE',
4034            update=update_components
4035            )
4036    beam_sec_alt : FloatProperty(
4037            name="Altitude",
4038            description="Distance from roof",
4039            default=-0.1,
4040            unit='LENGTH', subtype='DISTANCE',
4041            update=update_components
4042            )
4043    rafter_enable : BoolProperty(
4044            name="Rafter",
4045            default=True,
4046            update=update_components
4047            )
4048    rafter_width : FloatProperty(
4049            name="Width",
4050            description="Width",
4051            min=0.01,
4052            default=0.1,
4053            unit='LENGTH', subtype='DISTANCE',
4054            update=update_components
4055            )
4056    rafter_height : FloatProperty(
4057            name="Height",
4058            description="Height",
4059            min=0.01,
4060            default=0.2,
4061            unit='LENGTH', subtype='DISTANCE',
4062            update=update_components
4063            )
4064    rafter_spacing : FloatProperty(
4065            name="Spacing",
4066            description="Spacing",
4067            min=0.1,
4068            default=0.7,
4069            unit='LENGTH', subtype='DISTANCE',
4070            update=update_components
4071            )
4072    rafter_start : FloatProperty(
4073            name="Offset",
4074            description="Spacing from roof border",
4075            min=0,
4076            default=0.1,
4077            unit='LENGTH', subtype='DISTANCE',
4078            update=update_components
4079            )
4080    rafter_alt : FloatProperty(
4081            name="Altitude",
4082            description="Altitude from roof",
4083            max=-0.0001,
4084            default=-0.001,
4085            unit='LENGTH', subtype='DISTANCE',
4086            update=update_components
4087            )
4088
4089    hip_enable : BoolProperty(
4090            name="Enable",
4091            default=True,
4092            update=update_components
4093            )
4094    hip_expand : BoolProperty(
4095            options={'SKIP_SAVE'},
4096            name="Hips",
4097            description="Expand hips panel",
4098            default=False
4099            )
4100    hip_alt : FloatProperty(
4101            name="Altitude",
4102            description="Hip altitude from roof",
4103            default=0.1,
4104            unit='LENGTH', subtype='DISTANCE',
4105            update=update_components
4106            )
4107    hip_space_x : FloatProperty(
4108            name="Spacing",
4109            description="Space between hips",
4110            min=0.01,
4111            default=0.4,
4112            unit='LENGTH', subtype='DISTANCE',
4113            update=update_components
4114            )
4115    hip_size_x : FloatProperty(
4116            name="Length",
4117            description="Length of hip",
4118            min=0.01,
4119            default=0.4,
4120            unit='LENGTH', subtype='DISTANCE',
4121            update=update_components
4122            )
4123    hip_size_y : FloatProperty(
4124            name="Width",
4125            description="Width of hip",
4126            min=0.01,
4127            default=0.15,
4128            unit='LENGTH', subtype='DISTANCE',
4129            update=update_components
4130            )
4131    hip_size_z : FloatProperty(
4132            name="Height",
4133            description="Height of hip",
4134            min=0.0,
4135            default=0.15,
4136            unit='LENGTH', subtype='DISTANCE',
4137            update=update_components
4138            )
4139    hip_model : EnumProperty(
4140            name="Model",
4141            items=(
4142                ('ROUND', 'Round', '', 0),
4143                ('ETERNIT', 'Eternit', '', 1),
4144                ('FLAT', 'Flat', '', 2)
4145                ),
4146            default="ROUND",
4147            update=update_components
4148            )
4149    valley_altitude : FloatProperty(
4150            name="Altitude",
4151            description="Valley altitude from roof",
4152            default=0.1,
4153            unit='LENGTH', subtype='DISTANCE',
4154            update=update_components
4155            )
4156    valley_enable : BoolProperty(
4157            name="Valley",
4158            default=True,
4159            update=update_components
4160            )
4161
4162    fascia_enable : BoolProperty(
4163            name="Enable",
4164            description="Enable Fascia",
4165            default=True,
4166            update=update_components
4167            )
4168    fascia_expand : BoolProperty(
4169            options={'SKIP_SAVE'},
4170            name="Fascia",
4171            description="Expand fascia panel",
4172            default=False
4173            )
4174    fascia_height : FloatProperty(
4175            name="Height",
4176            description="Height",
4177            min=0.01,
4178            default=0.3,
4179            unit='LENGTH', subtype='DISTANCE',
4180            update=update_components
4181            )
4182    fascia_width : FloatProperty(
4183            name="Width",
4184            description="Width",
4185            min=0.01,
4186            default=0.02,
4187            unit='LENGTH', subtype='DISTANCE',
4188            update=update_components
4189            )
4190    fascia_offset : FloatProperty(
4191            name="Offset",
4192            description="Offset from roof border",
4193            default=0,
4194            unit='LENGTH', subtype='DISTANCE',
4195            update=update_components
4196            )
4197    fascia_altitude : FloatProperty(
4198            name="Altitude",
4199            description="Fascia altitude from roof",
4200            default=0.1,
4201            unit='LENGTH', subtype='DISTANCE',
4202            update=update_components
4203            )
4204
4205    bargeboard_enable : BoolProperty(
4206            name="Enable",
4207            description="Enable Bargeboard",
4208            default=True,
4209            update=update_components
4210            )
4211    bargeboard_expand : BoolProperty(
4212            options={'SKIP_SAVE'},
4213            name="Bargeboard",
4214            description="Expand Bargeboard panel",
4215            default=False
4216            )
4217    bargeboard_height : FloatProperty(
4218            name="Height",
4219            description="Height",
4220            min=0.01,
4221            default=0.3,
4222            unit='LENGTH', subtype='DISTANCE',
4223            update=update_components
4224            )
4225    bargeboard_width : FloatProperty(
4226            name="Width",
4227            description="Width",
4228            min=0.01,
4229            default=0.02,
4230            unit='LENGTH', subtype='DISTANCE',
4231            update=update_components
4232            )
4233    bargeboard_offset : FloatProperty(
4234            name="Offset",
4235            description="Offset from roof border",
4236            default=0.001,
4237            unit='LENGTH', subtype='DISTANCE',
4238            update=update_components
4239            )
4240    bargeboard_altitude : FloatProperty(
4241            name="Altitude",
4242            description="Fascia altitude from roof",
4243            default=0.1,
4244            unit='LENGTH', subtype='DISTANCE',
4245            update=update_components
4246            )
4247
4248    t_parent : StringProperty(
4249            name="Parent",
4250            default="",
4251            update=update_parent
4252            )
4253    t_part : IntProperty(
4254            name="Part",
4255            description="Parent part index",
4256            default=0,
4257            min=0,
4258            update=update_cutter
4259            )
4260    t_dist_x : FloatProperty(
4261            name="Dist x",
4262            description="Location on axis ",
4263            default=0,
4264            update=update_cutter
4265            )
4266    t_dist_y : FloatProperty(
4267            name="Dist y",
4268            description="Lateral distance from axis",
4269            min=0.0001,
4270            default=0.0001,
4271            update=update_cutter
4272            )
4273    z_parent: FloatProperty(
4274        description="Delta z of t child for grand childs",
4275        default=0
4276    )
4277    hole_offset_left : FloatProperty(
4278            name="Left",
4279            description="Left distance from border",
4280            min=0,
4281            default=0,
4282            update=update_cutter
4283            )
4284    hole_offset_right : FloatProperty(
4285            name="Right",
4286            description="Right distance from border",
4287            min=0,
4288            default=0,
4289            update=update_cutter
4290            )
4291    hole_offset_front : FloatProperty(
4292            name="Front",
4293            description="Front distance from border",
4294            default=0,
4295            update=update_cutter
4296            )
4297
4298    def make_wall_fit(self, context, o, wall, inside=False):
4299        origin = Vector((0, 0, self.z))
4300        g = self.get_generator(origin)
4301        g.make_roof(context)
4302        g.make_wall_fit(context, o, wall, inside)
4303
4304    def update_parts(self):
4305        # NOTE:
4306        # n_parts+1
4307        # as last one is end point of last segment or closing one
4308        for i in range(len(self.parts), self.n_parts, -1):
4309            self.parts.remove(i - 1)
4310
4311        # add rows
4312        for i in range(len(self.parts), self.n_parts):
4313            bound_idx = len(self.parts)
4314            self.parts.add()
4315            self.parts[-1].bound_idx = bound_idx
4316
4317        self.setup_manipulators()
4318
4319    def setup_manipulators(self):
4320        if len(self.manipulators) < 1:
4321            s = self.manipulators.add()
4322            s.type_key = "SIZE"
4323            s.prop1_name = "z"
4324            s.normal = (0, 1, 0)
4325        if len(self.manipulators) < 2:
4326            s = self.manipulators.add()
4327            s.type_key = "SIZE"
4328            s.prop1_name = "width_left"
4329        if len(self.manipulators) < 3:
4330            s = self.manipulators.add()
4331            s.type_key = "SIZE"
4332            s.prop1_name = "width_right"
4333
4334        for i in range(self.n_parts):
4335            p = self.parts[i]
4336            n_manips = len(p.manipulators)
4337            if n_manips < 1:
4338                s = p.manipulators.add()
4339                s.type_key = "ANGLE"
4340                s.prop1_name = "a0"
4341            if n_manips < 2:
4342                s = p.manipulators.add()
4343                s.type_key = "SIZE"
4344                s.prop1_name = "length"
4345            if n_manips < 3:
4346                s = p.manipulators.add()
4347                s.type_key = 'DUMB_STRING'
4348                s.prop1_name = str(i + 1)
4349            p.manipulators[2].prop1_name = str(i + 1)
4350            if n_manips < 4:
4351                s = p.manipulators.add()
4352                s.type_key = 'SIZE'
4353                s.prop1_name = "width_left"
4354            if n_manips < 5:
4355                s = p.manipulators.add()
4356                s.type_key = 'SIZE'
4357                s.prop1_name = "width_right"
4358            if n_manips < 6:
4359                s = p.manipulators.add()
4360                s.type_key = 'SIZE'
4361                s.prop1_name = "slope_left"
4362            if n_manips < 7:
4363                s = p.manipulators.add()
4364                s.type_key = 'SIZE'
4365                s.prop1_name = "slope_right"
4366
4367    def get_generator(self, origin=Vector((0, 0, 0))):
4368        g = RoofGenerator(self, origin)
4369
4370        # TODO: sort part by bound idx so deps always find parent
4371
4372        for i, part in enumerate(self.parts):
4373            # skip part if bound_idx > parent
4374            # so deps always see parent
4375            if part.bound_idx <= i:
4376                g.add_part(part)
4377        g.locate_manipulators()
4378        return g
4379
4380    def make_surface(self, o, verts, edges):
4381        bm = bmesh.new()
4382        for v in verts:
4383            bm.verts.new(v)
4384        bm.verts.ensure_lookup_table()
4385        for ed in edges:
4386            bm.edges.new((bm.verts[ed[0]], bm.verts[ed[1]]))
4387        bm.edges.ensure_lookup_table()
4388        # bmesh.ops.contextual_create(bm, geom=bm.edges)
4389        bm.to_mesh(o.data)
4390        bm.free()
4391
4392    def find_parent(self, context):
4393        o = context.scene.objects.get(self.t_parent.strip())
4394        return o, archipack_roof.datablock(o)
4395
4396    def intersection_angle(self, t_slope, t_width, p_slope, angle):
4397        # 2d intersection angle between two roofs parts
4398        dy = abs(t_slope * t_width / p_slope)
4399        ca = cos(angle)
4400        ta = tan(angle)
4401        if ta == 0:
4402            w0 = 0
4403        else:
4404            w0 = dy * ta
4405        if ca == 0:
4406            w1 = 0
4407        else:
4408            w1 = t_width / ca
4409        dx = w1 - w0
4410        return atan2(dy, dx)
4411
4412    def relocate_child(self, context, o, g, child):
4413
4414        d = archipack_roof.datablock(child)
4415
4416        if d is not None and d.t_part - 1 < len(g.segs):
4417            # print("relocate_child(%s)" % (child.name))
4418
4419            seg = g.segs[d.t_part]
4420            # adjust T part matrix_world from parent
4421            # T part origin located on parent axis
4422            # with y in parent direction
4423            t = (d.t_dist_x / seg.length)
4424            x, y, z = seg.lerp(t).to_3d()
4425            dy = -seg.v.normalized()
4426            child.matrix_world = o.matrix_world @ Matrix([
4427                [dy.x, -dy.y, 0, x],
4428                [dy.y, dy.x, 0, y],
4429                [0, 0, 1, z],
4430                [0, 0, 0, 1]
4431            ])
4432
4433    def relocate_childs(self, context, o, g):
4434        for child in o.children:
4435            d = archipack_roof.datablock(child)
4436            if d is not None and d.t_parent == o.name:
4437                self.relocate_child(context, o, g, child)
4438
4439    def update_childs(self, context, o, g):
4440        for child in o.children:
4441            d = archipack_roof.datablock(child)
4442            if d is not None and d.t_parent == o.name:
4443                # print("upate_childs(%s)" % (child.name))
4444                child.select_set(state=True)
4445                context.view_layer.objects.active = child
4446                # regenerate hole
4447                d.update(context, update_hole=True, update_parent=False)
4448                child.select_set(state=False)
4449        o.select_set(state=True)
4450        context.view_layer.objects.active = o
4451
4452    def update(self,
4453                context,
4454                manipulable_refresh=False,
4455                update_childs=False,
4456                update_parent=True,
4457                update_hole=False,
4458                force_update=False):
4459        """
4460            update_hole: on t_child must update parent
4461            update_childs: force childs update
4462            force_update: skip throttle
4463        """
4464        # print("update")
4465        o = self.find_in_selection(context, self.auto_update)
4466
4467        if o is None:
4468            return
4469
4470        # clean up manipulators before any data model change
4471        if manipulable_refresh:
4472            self.manipulable_disable(context)
4473
4474        self.update_parts()
4475
4476        verts, edges, faces, matids, uvs = [], [], [], [], []
4477
4478        y = 0
4479        z = self.z
4480        p, d = self.find_parent(context)
4481        g = None
4482
4483        # t childs: use parent to relocate
4484        # setup slopes into generator
4485        if d is not None:
4486            pg = d.get_generator()
4487            pg.make_roof(context)
4488
4489            if self.t_part - 1 < len(pg.segs):
4490
4491                seg = pg.nodes[self.t_part].root
4492
4493                d.relocate_child(context, p, pg, o)
4494
4495                a0 = self.parts[0].a0
4496                a_axis = a0 - pi / 2
4497                a_offset = 0
4498                s_left = self.slope_left
4499                w_left = -self.width_left
4500                s_right = self.slope_right
4501                w_right = self.width_right
4502                if a0 > 0:
4503                    # a_axis est mesure depuis la perpendiculaire à l'axe
4504                    slope = seg.right.slope
4505                    y = self.t_dist_y
4506                else:
4507                    a_offset = pi
4508                    slope = seg.left.slope
4509                    y = -self.t_dist_y
4510                    s_left, s_right = s_right, s_left
4511                    w_left, w_right = -w_right, -w_left
4512
4513                if slope == 0:
4514                    slope = 0.0001
4515
4516                # print("slope: %s" % (slope))
4517
4518                z = d.z_parent + d.z - self.t_dist_y * slope
4519                self.z_parent = z - self.z
4520                # a_right from axis cross z
4521
4522                b_right = self.intersection_angle(
4523                    s_left,
4524                    w_left,
4525                    slope,
4526                    a_axis)
4527
4528                a_right = b_right + a_offset
4529
4530                b_left = self.intersection_angle(
4531                    s_right,
4532                    w_right,
4533                    slope,
4534                    a_axis)
4535
4536                a_left = b_left + a_offset
4537
4538                g = self.get_generator(origin=Vector((0, y, z)))
4539
4540                # override by user defined slope if any
4541                make_right = True
4542                make_left = True
4543                for s in g.segs:
4544                    if (s.constraint_type == 'SLOPE' and
4545                            s.v0_idx == 0):
4546                        da = g.segs[0].v.angle_signed(s.v)
4547                        if da > 0:
4548                            make_left = False
4549                        else:
4550                            make_right = False
4551
4552                if make_left:
4553                    # Add 'SLOPE' constraints for segment 0
4554                    v = Vector((cos(a_left), sin(a_left)))
4555                    s = StraightRoof(g.origin, v)
4556                    s.v0_idx = 0
4557                    s.constraint_type = 'SLOPE'
4558                    # s.enforce_part = 'VALLEY'
4559                    s.angle_0 = a_left
4560                    s.take_precedence = False
4561                    g.segs.append(s)
4562
4563                if make_right:
4564                    v = Vector((cos(a_right), sin(a_right)))
4565                    s = StraightRoof(g.origin, v)
4566                    s.v0_idx = 0
4567                    s.constraint_type = 'SLOPE'
4568                    # s.enforce_part = 'VALLEY'
4569                    s.angle_0 = a_right
4570                    s.take_precedence = False
4571                    g.segs.append(s)
4572
4573        if g is None:
4574            g = self.get_generator(origin=Vector((0, y, z)))
4575
4576        # setup per segment manipulators
4577        if len(g.segs) > 0:
4578            f = g.segs[0]
4579            # z
4580            n = f.straight(-1, 0).v.to_3d()
4581            self.manipulators[0].set_pts([(0, 0, 0), (0, 0, self.z), (1, 0, 0)], normal=n)
4582            # left width
4583            n = f.sized_normal(0, -self.width_left)
4584            self.manipulators[1].set_pts([n.p0.to_3d(), n.p1.to_3d(), (-1, 0, 0)])
4585            # right width
4586            n = f.sized_normal(0, self.width_right)
4587            self.manipulators[2].set_pts([n.p0.to_3d(), n.p1.to_3d(), (1, 0, 0)])
4588
4589        g.make_roof(context)
4590
4591        # update childs here so parent may use
4592        # new holes when parent shape does change
4593        if update_childs:
4594            self.update_childs(context, o, g)
4595
4596        # on t_child
4597        if d is not None and update_hole:
4598            hole_obj = self.find_hole(context, o)
4599            g.make_hole(context, hole_obj, o, self, update_parent)
4600            # print("make_hole")
4601
4602        # add cutters
4603        g.boundary(context, o)
4604
4605        if self.draft:
4606
4607            g.draft(context, verts, edges)
4608            g.gutter(self, verts, faces, edges, matids, uvs)
4609            self.make_surface(o, verts, edges)
4610
4611        else:
4612
4613            if self.bargeboard_enable:
4614                g.bargeboard(self, verts, faces, edges, matids, uvs)
4615
4616            if self.fascia_enable:
4617                g.fascia(self, verts, faces, edges, matids, uvs)
4618
4619            if self.beam_enable:
4620                g.beam_primary(self, verts, faces, edges, matids, uvs)
4621
4622            g.hips(self, verts, faces, edges, matids, uvs)
4623
4624            if self.gutter_enable:
4625                g.gutter(self, verts, faces, edges, matids, uvs)
4626
4627            bmed.buildmesh(
4628
4629                context, o, verts, faces, matids=matids, uvs=uvs,
4630                weld=False, clean=False, auto_smooth=True, temporary=False)
4631
4632            # bpy.ops.object.mode_set(mode='EDIT')
4633            g.lambris(context, o, self)
4634            # print("lambris")
4635
4636            if self.rafter_enable:
4637                # bpy.ops.object.mode_set(mode='EDIT')
4638                g.rafter(context, o, self)
4639                # print("rafter")
4640
4641            if self.quick_edit and not force_update:
4642                if self.tile_enable:
4643                    bpy.ops.archipack.roof_throttle_update(name=o.name)
4644            else:
4645                # throttle here
4646                if self.tile_enable:
4647                    g.couverture(context, o, self)
4648                    # print("couverture")
4649
4650        # enable manipulators rebuild
4651        if manipulable_refresh:
4652            self.manipulable_refresh = True
4653        # print("rafter")
4654        # restore context
4655        self.restore_context(context)
4656        # print("restore context")
4657
4658    def find_hole(self, context, o):
4659        p, d = self.find_parent(context)
4660        if d is not None:
4661            for child in p.children:
4662                cd = archipack_roof_cutter.datablock(child)
4663                if cd is not None and cd.boundary == o.name:
4664                    return child
4665        return None
4666
4667    def manipulable_setup(self, context):
4668        """
4669            NOTE:
4670            this one assume context.active_object is the instance this
4671            data belongs to, failing to do so will result in wrong
4672            manipulators set on active object
4673        """
4674        self.manipulable_disable(context)
4675
4676        o = context.active_object
4677
4678        self.setup_manipulators()
4679
4680        for i, part in enumerate(self.parts):
4681
4682            if i > 0:
4683                # start angle
4684                self.manip_stack.append(part.manipulators[0].setup(context, o, part))
4685
4686            if part.constraint_type == 'HORIZONTAL':
4687                # length / radius + angle
4688                self.manip_stack.append(part.manipulators[1].setup(context, o, part))
4689
4690            # index
4691            self.manip_stack.append(part.manipulators[2].setup(context, o, self))
4692
4693            # size left
4694            if part.auto_left in {'WIDTH', 'ALL'}:
4695                self.manip_stack.append(part.manipulators[3].setup(context, o, part))
4696            # size right
4697            if part.auto_right in {'WIDTH', 'ALL'}:
4698                self.manip_stack.append(part.manipulators[4].setup(context, o, part))
4699            # slope left
4700            if part.auto_left in {'SLOPE', 'ALL'}:
4701                self.manip_stack.append(part.manipulators[5].setup(context, o, part))
4702            # slope right
4703            if part.auto_right in {'SLOPE', 'ALL'}:
4704                self.manip_stack.append(part.manipulators[6].setup(context, o, part))
4705
4706        for m in self.manipulators:
4707            self.manip_stack.append(m.setup(context, o, self))
4708
4709    def draw(self, layout, context):
4710        box = layout.box()
4711        row = box.row()
4712        if self.parts_expand:
4713            row.prop(self, 'parts_expand', icon="TRIA_DOWN", text="Parts", emboss=False)
4714            box.prop(self, 'n_parts')
4715            # box.prop(self, 'closed')
4716            for i, part in enumerate(self.parts):
4717                part.draw(layout, context, i)
4718        else:
4719            row.prop(self, 'parts_expand', icon="TRIA_RIGHT", text="Parts", emboss=False)
4720
4721
4722def update_hole(self, context):
4723    # update parent's roof only when manipulated
4724    self.update(context, update_parent=True)
4725
4726
4727def update_operation(self, context):
4728    self.reverse(context, make_ccw=(self.operation == 'INTERSECTION'))
4729
4730
4731class archipack_roof_cutter_segment(ArchipackCutterPart, PropertyGroup):
4732    manipulators : CollectionProperty(type=archipack_manipulator)
4733    type : EnumProperty(
4734        name="Type",
4735        items=(
4736            ('SIDE', 'Side', 'Side with bargeboard', 0),
4737            ('BOTTOM', 'Bottom', 'Bottom with gutter', 1),
4738            ('LINK', 'Side link', 'Side without decoration', 2),
4739            ('AXIS', 'Top', 'Top part with hip and beam', 3)
4740            # ('LINK_VALLEY', 'Side valley', 'Side with valley', 3),
4741            # ('LINK_HIP', 'Side hip', 'Side with hip', 4)
4742            ),
4743        default='SIDE',
4744        update=update_hole
4745        )
4746
4747    def find_in_selection(self, context):
4748        selected = context.selected_objects[:]
4749        for o in selected:
4750            d = archipack_roof_cutter.datablock(o)
4751            if d:
4752                for part in d.parts:
4753                    if part == self:
4754                        return d
4755        return None
4756
4757
4758class archipack_roof_cutter(ArchipackCutter, ArchipackObject, Manipulable, PropertyGroup):
4759    # boundary
4760    parts : CollectionProperty(type=archipack_roof_cutter_segment)
4761    boundary : StringProperty(
4762            default="",
4763            name="Boundary",
4764            description="Boundary of t child to cut parent"
4765            )
4766
4767    def update_points(self, context, o, pts, update_parent=False):
4768        """
4769            Create boundary from roof
4770        """
4771        self.auto_update = False
4772        self.manipulable_disable(context)
4773        self.from_points(pts)
4774        self.manipulable_refresh = True
4775        self.auto_update = True
4776        if update_parent:
4777            self.update_parent(context, o)
4778        # print("update_points")
4779
4780    def update_parent(self, context, o):
4781
4782        d = archipack_roof.datablock(o.parent)
4783        if d is not None:
4784            o.parent.select_set(state=True)
4785            context.view_layer.objects.active = o.parent
4786            d.update(context, update_childs=False, update_hole=False)
4787        o.parent.select_set(state=False)
4788        context.view_layer.objects.active = o
4789        # print("update_parent")
4790
4791
4792class ARCHIPACK_PT_roof_cutter(Panel):
4793    bl_idname = "ARCHIPACK_PT_roof_cutter"
4794    bl_label = "Roof Cutter"
4795    bl_space_type = 'VIEW_3D'
4796    bl_region_type = 'UI'
4797    bl_category = 'Archipack'
4798
4799    @classmethod
4800    def poll(cls, context):
4801        return archipack_roof_cutter.filter(context.active_object)
4802
4803    def draw(self, context):
4804        prop = archipack_roof_cutter.datablock(context.active_object)
4805        if prop is None:
4806            return
4807        layout = self.layout
4808        scene = context.scene
4809        box = layout.box()
4810        if prop.boundary != "":
4811            box.label(text="Auto Cutter:")
4812            box.label(text=prop.boundary)
4813        else:
4814            box.operator('archipack.roof_cutter_manipulate', icon='VIEW_PAN')
4815            box.prop(prop, 'operation', text="")
4816            box = layout.box()
4817            box.label(text="From curve")
4818            box.prop_search(prop, "user_defined_path", scene, "objects", text="", icon='OUTLINER_OB_CURVE')
4819            if prop.user_defined_path != "":
4820                box.prop(prop, 'user_defined_resolution')
4821                # box.prop(prop, 'x_offset')
4822                # box.prop(prop, 'angle_limit')
4823            """
4824            box.prop_search(prop, "boundary", scene, "objects", text="", icon='OUTLINER_OB_CURVE')
4825            """
4826            prop.draw(layout, context)
4827
4828
4829class ARCHIPACK_PT_roof(Panel):
4830    bl_idname = "ARCHIPACK_PT_roof"
4831    bl_label = "Roof"
4832    bl_space_type = 'VIEW_3D'
4833    bl_region_type = 'UI'
4834    bl_category = 'Archipack'
4835
4836    @classmethod
4837    def poll(cls, context):
4838        return archipack_roof.filter(context.active_object)
4839
4840    def draw(self, context):
4841        o = context.active_object
4842        prop = archipack_roof.datablock(o)
4843        if prop is None:
4844            return
4845        scene = context.scene
4846        layout = self.layout
4847        row = layout.row(align=True)
4848        row.operator('archipack.roof_manipulate', icon='VIEW_PAN')
4849
4850        box = layout.box()
4851        row = box.row(align=True)
4852        row.operator("archipack.roof_preset_menu", text=bpy.types.ARCHIPACK_OT_roof_preset_menu.bl_label)
4853        row.operator("archipack.roof_preset", text="", icon='ADD')
4854        row.operator("archipack.roof_preset", text="", icon='REMOVE').remove_active = True
4855        box = layout.box()
4856        box.prop_search(prop, "t_parent", scene, "objects", text="Parent", icon='OBJECT_DATA')
4857        layout.operator('archipack.roof_cutter').parent = o.name
4858        p, d = prop.find_parent(context)
4859        if d is not None:
4860            box.prop(prop, 't_part')
4861            box.prop(prop, 't_dist_x')
4862            box.prop(prop, 't_dist_y')
4863            box.label(text="Hole")
4864            box.prop(prop, 'hole_offset_front')
4865            box.prop(prop, 'hole_offset_left')
4866            box.prop(prop, 'hole_offset_right')
4867        box = layout.box()
4868        box.prop(prop, 'quick_edit', icon="MOD_MULTIRES")
4869        box.prop(prop, 'draft')
4870        if d is None:
4871            box.prop(prop, 'z')
4872        box.prop(prop, 'slope_left')
4873        box.prop(prop, 'slope_right')
4874        box.prop(prop, 'width_left')
4875        box.prop(prop, 'width_right')
4876        # parts
4877        prop.draw(layout, context)
4878        # tiles
4879        box = layout.box()
4880        row = box.row(align=True)
4881        if prop.tile_expand:
4882            row.prop(prop, 'tile_expand', icon="TRIA_DOWN", text="Covering", emboss=False)
4883        else:
4884            row.prop(prop, 'tile_expand', icon="TRIA_RIGHT", text="Covering", emboss=False)
4885        row.prop(prop, 'tile_enable')
4886        if prop.tile_expand:
4887            box.prop(prop, 'tile_model', text="")
4888
4889            box.prop(prop, 'tile_solidify', icon='MOD_SOLIDIFY')
4890            if prop.tile_solidify:
4891                box.prop(prop, 'tile_height')
4892                box.separator()
4893            box.prop(prop, 'tile_bevel', icon='MOD_BEVEL')
4894            if prop.tile_bevel:
4895                box.prop(prop, 'tile_bevel_amt')
4896                box.prop(prop, 'tile_bevel_segs')
4897                box.separator()
4898            box.label(text="Tile size")
4899            box.prop(prop, 'tile_size_x')
4900            box.prop(prop, 'tile_size_y')
4901            box.prop(prop, 'tile_size_z')
4902            box.prop(prop, 'tile_altitude')
4903
4904            box.separator()
4905            box.label(text="Distribution")
4906            box.prop(prop, 'tile_alternate', icon='NLA')
4907            row = box.row(align=True)
4908            row.prop(prop, 'tile_fit_x', icon='ALIGN')
4909            row.prop(prop, 'tile_fit_y', icon='ALIGN')
4910            box.prop(prop, 'tile_offset')
4911
4912            box.label(text="Spacing")
4913            box.prop(prop, 'tile_space_x')
4914            box.prop(prop, 'tile_space_y')
4915
4916            box.separator()     # hip
4917            box.label(text="Borders")
4918            box.prop(prop, 'tile_side')
4919            box.prop(prop, 'tile_couloir')
4920            box.prop(prop, 'tile_border')
4921
4922        box = layout.box()
4923        row = box.row(align=True)
4924        if prop.hip_expand:
4925            row.prop(prop, 'hip_expand', icon="TRIA_DOWN", text="Hip", emboss=False)
4926        else:
4927            row.prop(prop, 'hip_expand', icon="TRIA_RIGHT", text="Hip", emboss=False)
4928        row.prop(prop, 'hip_enable')
4929        if prop.hip_expand:
4930            box.prop(prop, 'hip_model', text="")
4931            box.prop(prop, 'hip_size_x')
4932            box.prop(prop, 'hip_size_y')
4933            box.prop(prop, 'hip_size_z')
4934            box.prop(prop, 'hip_alt')
4935            box.prop(prop, 'hip_space_x')
4936            box.separator()
4937            box.prop(prop, 'valley_enable')
4938            box.prop(prop, 'valley_altitude')
4939
4940        box = layout.box()
4941        row = box.row(align=True)
4942
4943        if prop.beam_expand:
4944            row.prop(prop, 'beam_expand', icon="TRIA_DOWN", text="Beam", emboss=False)
4945        else:
4946            row.prop(prop, 'beam_expand', icon="TRIA_RIGHT", text="Beam", emboss=False)
4947        if prop.beam_expand:
4948            box.prop(prop, 'beam_enable')
4949            if prop.beam_enable:
4950                box.prop(prop, 'beam_width')
4951                box.prop(prop, 'beam_height')
4952                box.prop(prop, 'beam_offset')
4953                box.prop(prop, 'beam_alt')
4954            box.separator()
4955            box.prop(prop, 'beam_sec_enable')
4956            if prop.beam_sec_enable:
4957                box.prop(prop, 'beam_sec_width')
4958                box.prop(prop, 'beam_sec_height')
4959                box.prop(prop, 'beam_sec_alt')
4960            box.separator()
4961            box.prop(prop, 'rafter_enable')
4962            if prop.rafter_enable:
4963                box.prop(prop, 'rafter_height')
4964                box.prop(prop, 'rafter_width')
4965                box.prop(prop, 'rafter_spacing')
4966                box.prop(prop, 'rafter_start')
4967                box.prop(prop, 'rafter_alt')
4968
4969        box = layout.box()
4970        row = box.row(align=True)
4971        if prop.gutter_expand:
4972            row.prop(prop, 'gutter_expand', icon="TRIA_DOWN", text="Gutter", emboss=False)
4973        else:
4974            row.prop(prop, 'gutter_expand', icon="TRIA_RIGHT", text="Gutter", emboss=False)
4975        row.prop(prop, 'gutter_enable')
4976        if prop.gutter_expand:
4977            box.prop(prop, 'gutter_alt')
4978            box.prop(prop, 'gutter_width')
4979            box.prop(prop, 'gutter_dist')
4980            box.prop(prop, 'gutter_boudin')
4981            box.prop(prop, 'gutter_segs')
4982
4983        box = layout.box()
4984        row = box.row(align=True)
4985        if prop.fascia_expand:
4986            row.prop(prop, 'fascia_expand', icon="TRIA_DOWN", text="Fascia", emboss=False)
4987        else:
4988            row.prop(prop, 'fascia_expand', icon="TRIA_RIGHT", text="Fascia", emboss=False)
4989        row.prop(prop, 'fascia_enable')
4990        if prop.fascia_expand:
4991            box.prop(prop, 'fascia_altitude')
4992            box.prop(prop, 'fascia_width')
4993            box.prop(prop, 'fascia_height')
4994            box.prop(prop, 'fascia_offset')
4995
4996        box = layout.box()
4997        row = box.row(align=True)
4998        if prop.bargeboard_expand:
4999            row.prop(prop, 'bargeboard_expand', icon="TRIA_DOWN", text="Bargeboard", emboss=False)
5000        else:
5001            row.prop(prop, 'bargeboard_expand', icon="TRIA_RIGHT", text="Bargeboard", emboss=False)
5002        row.prop(prop, 'bargeboard_enable')
5003        if prop.bargeboard_expand:
5004            box.prop(prop, 'bargeboard_altitude')
5005            box.prop(prop, 'bargeboard_width')
5006            box.prop(prop, 'bargeboard_height')
5007            box.prop(prop, 'bargeboard_offset')
5008
5009        """
5010        box = layout.box()
5011        row.prop_search(prop, "user_defined_path", scene, "objects", text="", icon='OUTLINER_OB_CURVE')
5012        box.prop(prop, 'user_defined_resolution')
5013        box.prop(prop, 'angle_limit')
5014        """
5015
5016
5017# ------------------------------------------------------------------
5018# Define operator class to create object
5019# ------------------------------------------------------------------
5020
5021
5022class ARCHIPACK_OT_roof(ArchipackCreateTool, Operator):
5023    bl_idname = "archipack.roof"
5024    bl_label = "Roof"
5025    bl_description = "Roof"
5026    bl_category = 'Archipack'
5027    bl_options = {'REGISTER', 'UNDO'}
5028
5029    def create(self, context):
5030        m = bpy.data.meshes.new("Roof")
5031        o = bpy.data.objects.new("Roof", m)
5032        d = m.archipack_roof.add()
5033        # make manipulators selectable
5034        d.manipulable_selectable = True
5035        self.link_object_to_scene(context, o)
5036        o.select_set(state=True)
5037        context.view_layer.objects.active = o
5038        self.add_material(o)
5039
5040        # disable progress bar when
5041        # background render thumbs
5042        if not self.auto_manipulate:
5043            d.quick_edit = False
5044
5045        self.load_preset(d)
5046        return o
5047
5048    # -----------------------------------------------------
5049    # Execute
5050    # -----------------------------------------------------
5051    def execute(self, context):
5052        if context.mode == "OBJECT":
5053            bpy.ops.object.select_all(action="DESELECT")
5054            o = self.create(context)
5055            o.location = context.scene.cursor.location
5056            o.select_set(state=True)
5057            context.view_layer.objects.active = o
5058            self.manipulate()
5059            return {'FINISHED'}
5060        else:
5061            self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
5062            return {'CANCELLED'}
5063
5064
5065class ARCHIPACK_OT_roof_cutter(ArchipackCreateTool, Operator):
5066    bl_idname = "archipack.roof_cutter"
5067    bl_label = "Roof Cutter"
5068    bl_description = "Roof Cutter"
5069    bl_category = 'Archipack'
5070    bl_options = {'REGISTER', 'UNDO'}
5071
5072    parent : StringProperty("")
5073
5074    def create(self, context):
5075        m = bpy.data.meshes.new("Roof Cutter")
5076        o = bpy.data.objects.new("Roof Cutter", m)
5077        d = m.archipack_roof_cutter.add()
5078        parent = context.scene.objects.get(self.parent.strip())
5079        if parent is not None:
5080            o.parent = parent
5081            bbox = parent.bound_box
5082            angle_90 = pi / 2
5083            x0, y0, z = bbox[0]
5084            x1, y1, z = bbox[6]
5085            x = 0.2 * (x1 - x0)
5086            y = 0.2 * (y1 - y0)
5087            o.matrix_world = parent.matrix_world @ Matrix([
5088                [1, 0, 0, -3 * x],
5089                [0, 1, 0, 0],
5090                [0, 0, 1, 0],
5091                [0, 0, 0, 1]
5092                ])
5093            p = d.parts.add()
5094            p.a0 = - angle_90
5095            p.length = y
5096            p = d.parts.add()
5097            p.a0 = angle_90
5098            p.length = x
5099            p = d.parts.add()
5100            p.a0 = angle_90
5101            p.length = y
5102            d.n_parts = 3
5103            # d.close = True
5104            pd = archipack_roof.datablock(parent)
5105            pd.boundary = o.name
5106        else:
5107            o.location = context.scene.cursor.location
5108        # make manipulators selectable
5109        d.manipulable_selectable = True
5110        self.link_object_to_scene(context, o)
5111        o.select_set(state=True)
5112        context.view_layer.objects.active = o
5113        self.add_material(o)
5114        self.load_preset(d)
5115        update_operation(d, context)
5116        return o
5117
5118    # -----------------------------------------------------
5119    # Execute
5120    # -----------------------------------------------------
5121    def execute(self, context):
5122        if context.mode == "OBJECT":
5123            bpy.ops.object.select_all(action="DESELECT")
5124            o = self.create(context)
5125            o.select_set(state=True)
5126            context.view_layer.objects.active = o
5127            self.manipulate()
5128            return {'FINISHED'}
5129        else:
5130            self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
5131            return {'CANCELLED'}
5132
5133
5134# ------------------------------------------------------------------
5135# Define operator class to create object
5136# ------------------------------------------------------------------
5137
5138
5139class ARCHIPACK_OT_roof_from_curve(ArchipackCreateTool, Operator):
5140    bl_idname = "archipack.roof_from_curve"
5141    bl_label = "Roof curve"
5142    bl_description = "Create a roof from a curve"
5143    bl_category = 'Archipack'
5144    bl_options = {'REGISTER', 'UNDO'}
5145
5146    auto_manipulate : BoolProperty(default=True)
5147
5148    @classmethod
5149    def poll(self, context):
5150        return context.active_object is not None and context.active_object.type == 'CURVE'
5151
5152    def draw(self, context):
5153        layout = self.layout
5154        row = layout.row()
5155        row.label(text="Use Properties panel (N) to define parms", icon='INFO')
5156
5157    def create(self, context):
5158        curve = context.active_object
5159        m = bpy.data.meshes.new("Roof")
5160        o = bpy.data.objects.new("Roof", m)
5161        d = m.archipack_roof.add()
5162        # make manipulators selectable
5163        d.manipulable_selectable = True
5164        d.user_defined_path = curve.name
5165        self.link_object_to_scene(context, o)
5166        o.select_set(state=True)
5167        context.view_layer.objects.active = o
5168        d.update_path(context)
5169
5170        spline = curve.data.splines[0]
5171        if spline.type == 'POLY':
5172            pt = spline.points[0].co
5173        elif spline.type == 'BEZIER':
5174            pt = spline.bezier_points[0].co
5175        else:
5176            pt = Vector((0, 0, 0))
5177        # pretranslate
5178        o.matrix_world = curve.matrix_world @ Matrix([
5179            [1, 0, 0, pt.x],
5180            [0, 1, 0, pt.y],
5181            [0, 0, 1, pt.z],
5182            [0, 0, 0, 1]
5183            ])
5184        o.select_set(state=True)
5185        context.view_layer.objects.active = o
5186        return o
5187
5188    # -----------------------------------------------------
5189    # Execute
5190    # -----------------------------------------------------
5191    def execute(self, context):
5192        if context.mode == "OBJECT":
5193            bpy.ops.object.select_all(action="DESELECT")
5194            self.create(context)
5195            if self.auto_manipulate:
5196                bpy.ops.archipack.roof_manipulate('INVOKE_DEFAULT')
5197            return {'FINISHED'}
5198        else:
5199            self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
5200            return {'CANCELLED'}
5201
5202
5203# ------------------------------------------------------------------
5204# Define operator class to manipulate object
5205# ------------------------------------------------------------------
5206
5207
5208class ARCHIPACK_OT_roof_manipulate(Operator):
5209    bl_idname = "archipack.roof_manipulate"
5210    bl_label = "Manipulate"
5211    bl_description = "Manipulate"
5212    bl_options = {'REGISTER', 'UNDO'}
5213
5214    @classmethod
5215    def poll(self, context):
5216        return archipack_roof.filter(context.active_object)
5217
5218    def invoke(self, context, event):
5219        d = archipack_roof.datablock(context.active_object)
5220        d.manipulable_invoke(context)
5221        return {'FINISHED'}
5222
5223
5224class ARCHIPACK_OT_roof_cutter_manipulate(Operator):
5225    bl_idname = "archipack.roof_cutter_manipulate"
5226    bl_label = "Manipulate"
5227    bl_description = "Manipulate"
5228    bl_options = {'REGISTER', 'UNDO'}
5229
5230    @classmethod
5231    def poll(self, context):
5232        return archipack_roof_cutter.filter(context.active_object)
5233
5234    def invoke(self, context, event):
5235        d = archipack_roof_cutter.datablock(context.active_object)
5236        d.manipulable_invoke(context)
5237        return {'FINISHED'}
5238
5239
5240# Update throttle
5241class ArchipackThrottleHandler():
5242    """
5243        One modal runs for each object at time
5244        when call for 2nd one
5245        update timer so first one wait more
5246        and kill 2nd one
5247    """
5248    def __init__(self, context, delay):
5249        self._timer = None
5250        self.start = 0
5251        self.update_state = False
5252        self.delay = delay
5253
5254    def start_timer(self, context):
5255        self.start = time.time()
5256        self._timer = context.window_manager.event_timer_add(self.delay, window=context.window)
5257
5258    def stop_timer(self, context):
5259        if self._timer is not None:
5260            context.window_manager.event_timer_remove(self._timer)
5261            self._timer = None
5262
5263    def execute(self, context):
5264        """
5265            refresh timer on execute
5266            return
5267                True if modal should run
5268                False on complete
5269        """
5270        if self._timer is None:
5271            self.update_state = False
5272            self.start_timer(context)
5273            return True
5274
5275        # already a timer running
5276        self.stop_timer(context)
5277
5278        # prevent race conditions when already in update mode
5279        if self.is_updating:
5280            return False
5281
5282        self.start_timer(context)
5283        return False
5284
5285    def modal(self, context, event):
5286        if event.type == 'TIMER' and not self.is_updating:
5287            if time.time() - self.start > self.delay:
5288                self.update_state = True
5289                self.stop_timer(context)
5290                return True
5291        return False
5292
5293    @property
5294    def is_updating(self):
5295        return self.update_state
5296
5297
5298throttle_handlers = {}
5299throttle_delay = 1
5300
5301
5302class ARCHIPACK_OT_roof_throttle_update(Operator):
5303    bl_idname = "archipack.roof_throttle_update"
5304    bl_label = "Update childs with a delay"
5305
5306    name : StringProperty()
5307
5308    def kill_handler(self, context, name):
5309        if name in throttle_handlers.keys():
5310            throttle_handlers[name].stop_timer(context)
5311            del throttle_handlers[self.name]
5312
5313    def get_handler(self, context, delay):
5314        global throttle_handlers
5315        if self.name not in throttle_handlers.keys():
5316            throttle_handlers[self.name] = ArchipackThrottleHandler(context, delay)
5317        return throttle_handlers[self.name]
5318
5319    def modal(self, context, event):
5320        global throttle_handlers
5321        if self.name in throttle_handlers.keys():
5322            if throttle_handlers[self.name].modal(context, event):
5323                act = context.active_object
5324                o = context.scene.objects.get(self.name.strip())
5325                # print("delay update of %s" % (self.name))
5326                if o is not None:
5327                    selected = o.select_get()
5328                    o.select_set(state=True)
5329                    context.view_layer.objects.active = o
5330                    d = o.data.archipack_roof[0]
5331                    d.update(context,
5332                        force_update=True,
5333                        update_parent=False)
5334                    # skip_parent_update=self.skip_parent_update)
5335                    o.select_set(state=selected)
5336                context.view_layer.objects.active = act
5337                del throttle_handlers[self.name]
5338                return {'FINISHED'}
5339            else:
5340                return {'PASS_THROUGH'}
5341        else:
5342            return {'FINISHED'}
5343
5344    def execute(self, context):
5345        global throttle_delay
5346        handler = self.get_handler(context, throttle_delay)
5347        if handler.execute(context):
5348            context.window_manager.modal_handler_add(self)
5349            return {'RUNNING_MODAL'}
5350        return {'FINISHED'}
5351
5352
5353# ------------------------------------------------------------------
5354# Define operator class to load / save presets
5355# ------------------------------------------------------------------
5356
5357
5358class ARCHIPACK_OT_roof_preset_menu(PresetMenuOperator, Operator):
5359    bl_description = "Show Roof presets"
5360    bl_idname = "archipack.roof_preset_menu"
5361    bl_label = "Roof Styles"
5362    preset_subdir = "archipack_roof"
5363
5364
5365class ARCHIPACK_OT_roof_preset(ArchipackPreset, Operator):
5366    """Add a Roof Styles"""
5367    bl_idname = "archipack.roof_preset"
5368    bl_label = "Add Roof Style"
5369    preset_menu = "ARCHIPACK_OT_roof_preset_menu"
5370
5371    @property
5372    def blacklist(self):
5373        return ['n_parts', 'parts', 'manipulators', 'user_defined_path', 'quick_edit', 'draft']
5374
5375
5376def register():
5377    # bpy.utils.register_class(archipack_roof_material)
5378    bpy.utils.register_class(archipack_roof_cutter_segment)
5379    bpy.utils.register_class(archipack_roof_cutter)
5380    bpy.utils.register_class(ARCHIPACK_PT_roof_cutter)
5381    bpy.utils.register_class(ARCHIPACK_OT_roof_cutter)
5382    bpy.utils.register_class(ARCHIPACK_OT_roof_cutter_manipulate)
5383    Mesh.archipack_roof_cutter = CollectionProperty(type=archipack_roof_cutter)
5384    bpy.utils.register_class(archipack_roof_segment)
5385    bpy.utils.register_class(archipack_roof)
5386    Mesh.archipack_roof = CollectionProperty(type=archipack_roof)
5387    bpy.utils.register_class(ARCHIPACK_OT_roof_preset_menu)
5388    bpy.utils.register_class(ARCHIPACK_PT_roof)
5389    bpy.utils.register_class(ARCHIPACK_OT_roof)
5390    bpy.utils.register_class(ARCHIPACK_OT_roof_preset)
5391    bpy.utils.register_class(ARCHIPACK_OT_roof_manipulate)
5392    bpy.utils.register_class(ARCHIPACK_OT_roof_from_curve)
5393    bpy.utils.register_class(ARCHIPACK_OT_roof_throttle_update)
5394
5395
5396def unregister():
5397    # bpy.utils.unregister_class(archipack_roof_material)
5398    bpy.utils.unregister_class(archipack_roof_cutter_segment)
5399    bpy.utils.unregister_class(archipack_roof_cutter)
5400    bpy.utils.unregister_class(ARCHIPACK_PT_roof_cutter)
5401    bpy.utils.unregister_class(ARCHIPACK_OT_roof_cutter)
5402    bpy.utils.unregister_class(ARCHIPACK_OT_roof_cutter_manipulate)
5403    del Mesh.archipack_roof_cutter
5404    bpy.utils.unregister_class(archipack_roof_segment)
5405    bpy.utils.unregister_class(archipack_roof)
5406    del Mesh.archipack_roof
5407    bpy.utils.unregister_class(ARCHIPACK_OT_roof_preset_menu)
5408    bpy.utils.unregister_class(ARCHIPACK_PT_roof)
5409    bpy.utils.unregister_class(ARCHIPACK_OT_roof)
5410    bpy.utils.unregister_class(ARCHIPACK_OT_roof_preset)
5411    bpy.utils.unregister_class(ARCHIPACK_OT_roof_manipulate)
5412    bpy.utils.unregister_class(ARCHIPACK_OT_roof_from_curve)
5413    bpy.utils.unregister_class(ARCHIPACK_OT_roof_throttle_update)
5414