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# ----------------------------------------------------------
27import bpy
28import bmesh
29
30import time
31
32from bpy.types import Operator, PropertyGroup, Mesh, Panel
33from bpy.props import (
34    FloatProperty, BoolProperty, IntProperty, StringProperty,
35    FloatVectorProperty, CollectionProperty, EnumProperty
36)
37from .bmesh_utils import BmeshEdit as bmed
38from mathutils import Vector, Matrix
39from mathutils.geometry import (
40    interpolate_bezier
41    )
42from math import sin, cos, pi, atan2
43from .archipack_manipulator import (
44    Manipulable, archipack_manipulator,
45    GlPolygon, GlPolyline,
46    GlLine, GlText, FeedbackPanel
47    )
48from .archipack_object import ArchipackObject, ArchipackCreateTool, ArchipackDrawTool
49from .archipack_2d import Line, Arc
50from .archipack_snap import snap_point
51from .archipack_keymaps import Keymaps
52
53import logging
54logger = logging.getLogger("archipack")
55
56
57class Wall():
58    def __init__(self, wall_z, z, t, flip):
59        self.z = z
60        self.wall_z = wall_z
61        self.t = t
62        self.flip = flip
63        self.z_step = len(z)
64
65    def set_offset(self, offset, last=None):
66        """
67            Offset line and compute intersection point
68            between segments
69        """
70        self.line = self.make_offset(offset, last)
71
72    def get_z(self, t):
73        t0 = self.t[0]
74        z0 = self.z[0]
75        for i in range(1, self.z_step):
76            t1 = self.t[i]
77            z1 = self.z[i]
78            if t <= t1:
79                return z0 + (t - t0) / (t1 - t0) * (z1 - z0)
80            t0, z0 = t1, z1
81        return self.z[-1]
82
83    def make_faces(self, i, f, faces):
84        if i < self.n_step:
85            # 1 3   5 7
86            # 0 2   4 6
87            if self.flip:
88                faces.append((f + 2, f, f + 1, f + 3))
89            else:
90                faces.append((f, f + 2, f + 3, f + 1))
91
92    def p3d(self, verts, t):
93        x, y = self.lerp(t)
94        z = self.wall_z + self.get_z(t)
95        verts.append((x, y, 0))
96        verts.append((x, y, z))
97
98    def make_wall(self, i, verts, faces):
99        t = self.t_step[i]
100        f = len(verts)
101        self.p3d(verts, t)
102        self.make_faces(i, f, faces)
103
104    def make_hole(self, i, verts, z0):
105        t = self.t_step[i]
106        x, y = self.line.lerp(t)
107        verts.append((x, y, z0))
108
109    def straight_wall(self, a0, length, wall_z, z, t):
110        r = self.straight(length).rotate(a0)
111        return StraightWall(r.p, r.v, wall_z, z, t, self.flip)
112
113    def curved_wall(self, a0, da, radius, wall_z, z, t):
114        n = self.normal(1).rotate(a0).scale(radius)
115        if da < 0:
116            n.v = -n.v
117        a0 = n.angle
118        c = n.p - n.v
119        return CurvedWall(c, radius, a0, da, wall_z, z, t, self.flip)
120
121
122class StraightWall(Wall, Line):
123    def __init__(self, p, v, wall_z, z, t, flip):
124        Line.__init__(self, p, v)
125        Wall.__init__(self, wall_z, z, t, flip)
126
127    def param_t(self, step_angle):
128        self.t_step = self.t
129        self.n_step = len(self.t) - 1
130
131
132class CurvedWall(Wall, Arc):
133    def __init__(self, c, radius, a0, da, wall_z, z, t, flip):
134        Arc.__init__(self, c, radius, a0, da)
135        Wall.__init__(self, wall_z, z, t, flip)
136
137    def param_t(self, step_angle):
138        t_step, n_step = self.steps_by_angle(step_angle)
139        self.t_step = list(sorted([i * t_step for i in range(1, n_step)] + self.t))
140        self.n_step = len(self.t_step) - 1
141
142
143class WallGenerator():
144    def __init__(self, parts):
145        self.last_type = 'NONE'
146        self.segs = []
147        self.parts = parts
148        self.faces_type = 'NONE'
149        self.closed = False
150
151    def set_offset(self, offset):
152        last = None
153        for i, seg in enumerate(self.segs):
154            seg.set_offset(offset, last)
155            last = seg.line
156
157        if self.closed:
158            w = self.segs[-1]
159            if len(self.segs) > 1:
160                w.line = w.make_offset(offset, self.segs[-2].line)
161
162            p1 = self.segs[0].line.p1
163            self.segs[0].line = self.segs[0].make_offset(offset, w.line)
164            self.segs[0].line.p1 = p1
165
166    def add_part(self, part, wall_z, flip):
167
168        # TODO:
169        # refactor this part (height manipulators)
170        manip_index = []
171        if len(self.segs) < 1:
172            s = None
173            z = [part.z[0]]
174            manip_index.append(0)
175        else:
176            s = self.segs[-1]
177            z = [s.z[-1]]
178
179        t_cur = 0
180        z_last = part.n_splits - 1
181        t = [0]
182
183        for i in range(part.n_splits):
184            t_try = t[-1] + part.t[i]
185            if t_try == t_cur:
186                continue
187            if t_try <= 1:
188                t_cur = t_try
189                t.append(t_cur)
190                z.append(part.z[i])
191                manip_index.append(i)
192            else:
193                z_last = i
194                break
195
196        if t_cur < 1:
197            t.append(1)
198            manip_index.append(z_last)
199            z.append(part.z[z_last])
200
201        # start a new wall
202        if s is None:
203            if part.type == 'S_WALL':
204                p = Vector((0, 0))
205                v = part.length * Vector((cos(part.a0), sin(part.a0)))
206                s = StraightWall(p, v, wall_z, z, t, flip)
207            elif part.type == 'C_WALL':
208                c = -part.radius * Vector((cos(part.a0), sin(part.a0)))
209                s = CurvedWall(c, part.radius, part.a0, part.da, wall_z, z, t, flip)
210        else:
211            if part.type == 'S_WALL':
212                s = s.straight_wall(part.a0, part.length, wall_z, z, t)
213            elif part.type == 'C_WALL':
214                s = s.curved_wall(part.a0, part.da, part.radius, wall_z, z, t)
215
216        self.segs.append(s)
217        self.last_type = part.type
218
219        return manip_index
220
221    def close(self, closed):
222        # Make last segment implicit closing one
223        if closed:
224            part = self.parts[-1]
225            w = self.segs[-1]
226            dp = self.segs[0].p0 - self.segs[-1].p0
227            if "C_" in part.type:
228                dw = (w.p1 - w.p0)
229                w.r = part.radius / dw.length * dp.length
230                # angle pt - p0        - angle p0 p1
231                da = atan2(dp.y, dp.x) - atan2(dw.y, dw.x)
232                a0 = w.a0 + da
233                if a0 > pi:
234                    a0 -= 2 * pi
235                if a0 < -pi:
236                    a0 += 2 * pi
237                w.a0 = a0
238            else:
239                w.v = dp
240
241    def locate_manipulators(self, side):
242
243        for i, wall in enumerate(self.segs):
244
245            manipulators = self.parts[i].manipulators
246
247            p0 = wall.p0.to_3d()
248            p1 = wall.p1.to_3d()
249
250            # angle from last to current segment
251            if i > 0:
252
253                if i < len(self.segs) - 1:
254                    manipulators[0].type_key = 'ANGLE'
255                else:
256                    manipulators[0].type_key = 'DUMB_ANGLE'
257
258                v0 = self.segs[i - 1].straight(-side, 1).v.to_3d()
259                v1 = wall.straight(side, 0).v.to_3d()
260                manipulators[0].set_pts([p0, v0, v1])
261
262            if type(wall).__name__ == "StraightWall":
263                # segment length
264                manipulators[1].type_key = 'SIZE'
265                manipulators[1].prop1_name = "length"
266                manipulators[1].set_pts([p0, p1, (side, 0, 0)])
267            else:
268                # segment radius + angle
269                # scale to fix overlap with drag
270                v0 = side * (wall.p0 - wall.c).to_3d()
271                v1 = side * (wall.p1 - wall.c).to_3d()
272                scale = 1.0 + (0.5 / v0.length)
273                manipulators[1].type_key = 'ARC_ANGLE_RADIUS'
274                manipulators[1].prop1_name = "da"
275                manipulators[1].prop2_name = "radius"
276                manipulators[1].set_pts([wall.c.to_3d(), scale * v0, scale * v1])
277
278            # snap manipulator, don't change index !
279            manipulators[2].set_pts([p0, p1, (1, 0, 0)])
280
281            # dumb, segment index
282            z = Vector((0, 0, 0.75 * wall.wall_z))
283            manipulators[3].set_pts([p0 + z, p1 + z, (1, 0, 0)])
284
285    def make_wall(self, step_angle, flip, closed, verts, faces):
286
287        # swap manipulators so they always face outside
288        side = 1
289        if flip:
290            side = -1
291
292        # Make last segment implicit closing one
293
294        nb_segs = len(self.segs) - 1
295        if closed:
296            nb_segs += 1
297
298        for i, wall in enumerate(self.segs):
299
300            wall.param_t(step_angle)
301            if i < nb_segs:
302                for j in range(wall.n_step + 1):
303                    wall.make_wall(j, verts, faces)
304            else:
305                # last segment
306                for j in range(wall.n_step):
307                    continue
308                    # print("%s" % (wall.n_step))
309                    # wall.make_wall(j, verts, faces)
310
311        self.locate_manipulators(side)
312
313    def rotate(self, idx_from, a):
314        """
315            apply rotation to all following segs
316        """
317        self.segs[idx_from].rotate(a)
318        ca = cos(a)
319        sa = sin(a)
320        rM = Matrix([
321            [ca, -sa],
322            [sa, ca]
323            ])
324        # rotation center
325        p0 = self.segs[idx_from].p0
326        for i in range(idx_from + 1, len(self.segs)):
327            seg = self.segs[i]
328            seg.rotate(a)
329            dp = rM @ (seg.p0 - p0)
330            seg.translate(dp)
331
332    def translate(self, idx_from, dp):
333        """
334            apply translation to all following segs
335        """
336        self.segs[idx_from].p1 += dp
337        for i in range(idx_from + 1, len(self.segs)):
338            self.segs[i].translate(dp)
339
340    def change_coordsys(self, fromTM, toTM):
341        """
342            move shape fromTM into toTM coordsys
343        """
344        dp = (toTM.inverted() @ fromTM.translation).to_2d()
345        da = toTM.row[1].to_2d().angle_signed(fromTM.row[1].to_2d())
346        ca = cos(da)
347        sa = sin(da)
348        rM = Matrix([
349            [ca, -sa],
350            [sa, ca]
351            ])
352        for s in self.segs:
353            tp = (rM @ s.p0) - s.p0 + dp
354            s.rotate(da)
355            s.translate(tp)
356
357    def draw(self, context):
358        for seg in self.segs:
359            seg.draw(context, render=False)
360
361    def debug(self, verts):
362        for wall in self.segs:
363            for i in range(33):
364                x, y = wall.lerp(i / 32)
365                verts.append((x, y, 0))
366
367    def make_surface(self, o, verts, height):
368        bm = bmesh.new()
369        for v in verts:
370            bm.verts.new(v)
371        bm.verts.ensure_lookup_table()
372        for i in range(1, len(verts)):
373            bm.edges.new((bm.verts[i - 1], bm.verts[i]))
374        bm.edges.new((bm.verts[-1], bm.verts[0]))
375        bm.edges.ensure_lookup_table()
376        bmesh.ops.contextual_create(bm, geom=bm.edges)
377        geom = bm.faces[:]
378        bmesh.ops.solidify(bm, geom=geom, thickness=height)
379        bm.to_mesh(o.data)
380        bm.free()
381
382    def make_hole(self, context, hole_obj, d):
383
384        offset = -0.5 * (1 - d.x_offset) * d.width
385
386        z0 = 0.1
387        self.set_offset(offset)
388
389        nb_segs = len(self.segs) - 1
390        if d.closed:
391            nb_segs += 1
392
393        verts = []
394        for i, wall in enumerate(self.segs):
395            wall.param_t(d.step_angle)
396            if i < nb_segs:
397                for j in range(wall.n_step + 1):
398                    wall.make_hole(j, verts, -z0)
399
400        self.make_surface(hole_obj, verts, d.z + z0)
401
402
403def update(self, context):
404    self.update(context)
405
406
407def update_childs(self, context):
408    self.update(context, update_childs=True, manipulable_refresh=True)
409
410
411def update_manipulators(self, context):
412    self.update(context, manipulable_refresh=True)
413
414
415def update_t_part(self, context):
416    """
417        Make this wall a T child of parent wall
418        orient child so y points inside wall and x follow wall segment
419        set child a0 according
420    """
421    o = self.find_in_selection(context)
422    if o is not None:
423
424        # w is parent wall
425        w = context.scene.objects.get(self.t_part.strip())
426        wd = archipack_wall2.datablock(w)
427
428        if wd is not None:
429            og = self.get_generator()
430            self.setup_childs(o, og)
431
432            bpy.ops.object.select_all(action="DESELECT")
433
434            # 5 cases here:
435            # 1 No parents at all
436            # 2 o has parent
437            # 3 w has parent
438            # 4 o and w share same parent already
439            # 5 o and w doesn't share parent
440            link_to_parent = False
441
442            # when both walls do have a reference point, we may delete one of them
443            to_delete = None
444
445            # select childs and make parent reference point active
446            if w.parent is None:
447                # Either link to o.parent or create new parent
448                link_to_parent = True
449                if o.parent is None:
450                    # create a reference point and make it active
451                    x, y, z = w.bound_box[0]
452                    context.scene.cursor.location = w.matrix_world @ Vector((x, y, z))
453                    # fix issue #9
454                    context.view_layer.objects.active = o
455                    bpy.ops.archipack.reference_point()
456                    o.select_set(state=True)
457                else:
458                    context.view_layer.objects.active = o.parent
459                w.select_set(state=True)
460            else:
461                # w has parent
462                if o.parent is not w.parent:
463                    link_to_parent = True
464                    context.view_layer.objects.active = w.parent
465                    o.select_set(state=True)
466                    if o.parent is not None:
467                        # store o.parent to delete it
468                        to_delete = o.parent
469                        for c in o.parent.children:
470                            if c is not o:
471                                c.hide_select = False
472                                c.select_set(state=True)
473
474            parent = context.active_object
475
476            dmax = 2 * wd.width
477
478            wg = wd.get_generator()
479
480            otM = o.matrix_world
481            orM = Matrix([
482                otM[0].to_2d(),
483                otM[1].to_2d()
484                ])
485
486            wtM = w.matrix_world
487            wrM = Matrix([
488                wtM[0].to_2d(),
489                wtM[1].to_2d()
490                ])
491
492            # dir in absolute world coordsys
493            dir = orM @ og.segs[0].straight(1, 0).v
494
495            # pt in w coordsys
496            pos = otM.translation
497            pt = (wtM.inverted() @ pos).to_2d()
498
499            for wall_idx, wall in enumerate(wg.segs):
500                res, dist, t = wall.point_sur_segment(pt)
501                # outside is on the right side of the wall
502                #  p1
503                #  |-- x
504                #  p0
505
506                # NOTE:
507                # rotation here is wrong when w has not parent while o has parent
508
509                if res and t > 0 and t < 1 and abs(dist) < dmax:
510                    x = wrM @ wall.straight(1, t).v
511                    y = wrM @ wall.normal(t).v.normalized()
512                    self.parts[0].a0 = dir.angle_signed(x)
513                    o.matrix_world = Matrix([
514                        [x.x, -y.x, 0, pos.x],
515                        [x.y, -y.y, 0, pos.y],
516                        [0, 0, 1, pos.z],
517                        [0, 0, 0, 1]
518                    ])
519                    break
520
521            if link_to_parent and bpy.ops.archipack.parent_to_reference.poll():
522                bpy.ops.archipack.parent_to_reference('INVOKE_DEFAULT')
523
524            # update generator to take new rotation in account
525            # use this to relocate windows on wall after reparenting
526            g = self.get_generator()
527            self.relocate_childs(context, o, g)
528
529            # hide holes from select
530            for c in parent.children:
531                if "archipack_hybridhole" in c:
532                    c.hide_select = True
533
534            # delete unneeded reference point
535            if to_delete is not None:
536                bpy.ops.object.select_all(action="DESELECT")
537                to_delete.select_set(state=True)
538                context.view_layer.objects.active = to_delete
539                if bpy.ops.object.delete.poll():
540                    bpy.ops.object.delete(use_global=False)
541
542        elif self.t_part != "":
543            self.t_part = ""
544
545    self.restore_context(context)
546
547
548def set_splits(self, value):
549    if self.n_splits != value:
550        self.auto_update = False
551        self._set_t(value)
552        self.auto_update = True
553        self.n_splits = value
554    return None
555
556
557def get_splits(self):
558    return self.n_splits
559
560
561def update_type(self, context):
562
563    d = self.find_datablock_in_selection(context)
564
565    if d is not None and d.auto_update:
566
567        d.auto_update = False
568        idx = 0
569        for i, part in enumerate(d.parts):
570            if part == self:
571                idx = i
572                break
573        a0 = 0
574        if idx > 0:
575            g = d.get_generator()
576            w0 = g.segs[idx - 1]
577            a0 = w0.straight(1).angle
578            if "C_" in self.type:
579                w = w0.straight_wall(self.a0, self.length, d.z, self.z, self.t)
580            else:
581                w = w0.curved_wall(self.a0, self.da, self.radius, d.z, self.z, self.t)
582        else:
583            if "C_" in self.type:
584                p = Vector((0, 0))
585                v = self.length * Vector((cos(self.a0), sin(self.a0)))
586                w = StraightWall(p, v, d.z, self.z, self.t, d.flip)
587                a0 = pi / 2
588            else:
589                c = -self.radius * Vector((cos(self.a0), sin(self.a0)))
590                w = CurvedWall(c, self.radius, self.a0, pi, d.z, self.z, self.t, d.flip)
591
592        # w0 - w - w1
593        if d.closed and idx == d.n_parts:
594            dp = - w.p0
595        else:
596            dp = w.p1 - w.p0
597
598        if "C_" in self.type:
599            self.radius = 0.5 * dp.length
600            self.da = pi
601            a0 = atan2(dp.y, dp.x) - pi / 2 - a0
602        else:
603            self.length = dp.length
604            a0 = atan2(dp.y, dp.x) - a0
605
606        if a0 > pi:
607            a0 -= 2 * pi
608        if a0 < -pi:
609            a0 += 2 * pi
610        self.a0 = a0
611
612        if idx + 1 < d.n_parts:
613            # adjust rotation of next part
614            part1 = d.parts[idx + 1]
615            if "C_" in self.type:
616                a0 = part1.a0 - pi / 2
617            else:
618                a0 = part1.a0 + w.straight(1).angle - atan2(dp.y, dp.x)
619
620            if a0 > pi:
621                a0 -= 2 * pi
622            if a0 < -pi:
623                a0 += 2 * pi
624            part1.a0 = a0
625
626        d.auto_update = True
627
628
629class archipack_wall2_part(PropertyGroup):
630    type : EnumProperty(
631            items=(
632                ('S_WALL', 'Straight', '', 0),
633                ('C_WALL', 'Curved', '', 1)
634                ),
635            default='S_WALL',
636            update=update_type
637            )
638    length : FloatProperty(
639            name="Length",
640            min=0.01,
641            default=2.0,
642            unit='LENGTH', subtype='DISTANCE',
643            update=update
644            )
645    radius : FloatProperty(
646            name="Radius",
647            min=0.5,
648            default=0.7,
649            unit='LENGTH', subtype='DISTANCE',
650            update=update
651            )
652    a0 : FloatProperty(
653            name="Start angle",
654            min=-pi,
655            max=pi,
656            default=pi / 2,
657            subtype='ANGLE', unit='ROTATION',
658            update=update
659            )
660    da : FloatProperty(
661            name="Angle",
662            min=-pi,
663            max=pi,
664            default=pi / 2,
665            subtype='ANGLE', unit='ROTATION',
666            update=update
667            )
668    z : FloatVectorProperty(
669            name="Height",
670            default=[
671                0, 0, 0, 0, 0, 0, 0, 0,
672                0, 0, 0, 0, 0, 0, 0, 0,
673                0, 0, 0, 0, 0, 0, 0, 0,
674                0, 0, 0, 0, 0, 0, 0
675            ],
676            size=31,
677            update=update
678            )
679    t : FloatVectorProperty(
680            name="Position",
681            min=0,
682            max=1,
683            default=[
684                1, 1, 1, 1, 1, 1, 1, 1,
685                1, 1, 1, 1, 1, 1, 1, 1,
686                1, 1, 1, 1, 1, 1, 1, 1,
687                1, 1, 1, 1, 1, 1, 1
688            ],
689            size=31,
690            update=update
691            )
692    splits : IntProperty(
693            name="Splits",
694            default=1,
695            min=1,
696            max=31,
697            get=get_splits, set=set_splits
698            )
699    n_splits : IntProperty(
700            name="Splits",
701            default=1,
702            min=1,
703            max=31,
704            update=update
705            )
706    auto_update : BoolProperty(default=True)
707    manipulators : CollectionProperty(type=archipack_manipulator)
708    # ui related
709    expand : BoolProperty(default=False)
710
711    def _set_t(self, splits):
712        t = 1 / splits
713        for i in range(splits):
714            self.t[i] = t
715
716    def find_datablock_in_selection(self, context):
717        """
718            find witch selected object this instance belongs to
719            provide support for "copy to selected"
720        """
721        selected = context.selected_objects[:]
722        for o in selected:
723            props = archipack_wall2.datablock(o)
724            if props:
725                for part in props.parts:
726                    if part == self:
727                        return props
728        return None
729
730    def update(self, context, manipulable_refresh=False):
731        if not self.auto_update:
732            return
733        props = self.find_datablock_in_selection(context)
734        if props is not None:
735            props.update(context, manipulable_refresh)
736
737    def draw(self, layout, context, index):
738
739        row = layout.row(align=True)
740        if self.expand:
741            row.prop(self, 'expand', icon="TRIA_DOWN", text="Part " + str(index + 1), emboss=False)
742        else:
743            row.prop(self, 'expand', icon="TRIA_RIGHT", text="Part " + str(index + 1), emboss=False)
744
745        row.prop(self, "type", text="")
746
747        if self.expand:
748            row = layout.row(align=True)
749            row.operator("archipack.wall2_insert", text="Split").index = index
750            row.operator("archipack.wall2_remove", text="Remove").index = index
751            if self.type == 'C_WALL':
752                row = layout.row()
753                row.prop(self, "radius")
754                row = layout.row()
755                row.prop(self, "da")
756            else:
757                row = layout.row()
758                row.prop(self, "length")
759            row = layout.row()
760            row.prop(self, "a0")
761            row = layout.row()
762            row.prop(self, "splits")
763            for split in range(self.n_splits):
764                row = layout.row()
765                row.prop(self, "z", text="alt", index=split)
766                row.prop(self, "t", text="pos", index=split)
767
768
769class archipack_wall2_child(PropertyGroup):
770    # Size  Loc
771    # Delta Loc
772    manipulators : CollectionProperty(type=archipack_manipulator)
773    child_name : StringProperty()
774    wall_idx : IntProperty()
775    pos : FloatVectorProperty(subtype='XYZ')
776    flip : BoolProperty(default=False)
777
778    def get_child(self, context):
779        d = None
780        child = context.scene.objects.get(self.child_name.strip())
781        if child is not None and child.data is not None:
782            cd = child.data
783            if 'archipack_window' in cd:
784                d = cd.archipack_window[0]
785            elif 'archipack_door' in cd:
786                d = cd.archipack_door[0]
787        return child, d
788
789
790class archipack_wall2(ArchipackObject, Manipulable, PropertyGroup):
791    parts : CollectionProperty(type=archipack_wall2_part)
792    n_parts : IntProperty(
793            name="Parts",
794            min=1,
795            max=1024,
796            default=1, update=update_manipulators
797            )
798    step_angle : FloatProperty(
799            description="Curved parts segmentation",
800            name="Step angle",
801            min=1 / 180 * pi,
802            max=pi,
803            default=6 / 180 * pi,
804            subtype='ANGLE', unit='ROTATION',
805            update=update
806            )
807    width : FloatProperty(
808            name="Width",
809            min=0.01,
810            default=0.2,
811            unit='LENGTH', subtype='DISTANCE',
812            update=update
813            )
814    z : FloatProperty(
815            name='Height',
816            min=0.1,
817            default=2.7, precision=2,
818            unit='LENGTH', subtype='DISTANCE',
819            description='height', update=update,
820            )
821    x_offset : FloatProperty(
822            name="Offset",
823            min=-1, max=1,
824            default=-1, precision=2, step=1,
825            update=update
826            )
827    radius : FloatProperty(
828            name="Radius",
829            min=0.5,
830            default=0.7,
831            unit='LENGTH', subtype='DISTANCE',
832            update=update
833            )
834    da : FloatProperty(
835            name="Angle",
836            min=-pi,
837            max=pi,
838            default=pi / 2,
839            subtype='ANGLE', unit='ROTATION',
840            update=update
841            )
842    flip : BoolProperty(
843            name="Flip",
844            default=False,
845            update=update_childs
846            )
847    closed : BoolProperty(
848            default=False,
849            name="Close",
850            update=update_manipulators
851            )
852    auto_update : BoolProperty(
853            options={'SKIP_SAVE'},
854            default=True,
855            update=update_manipulators
856            )
857    realtime : BoolProperty(
858            options={'SKIP_SAVE'},
859            default=True,
860            name="Real Time",
861            description="Relocate childs in realtime"
862            )
863    # dumb manipulators to show sizes between childs
864    childs_manipulators : CollectionProperty(type=archipack_manipulator)
865    # store to manipulate windows and doors
866    childs : CollectionProperty(type=archipack_wall2_child)
867    t_part : StringProperty(
868            name="Parent wall",
869            description="This part will follow parent when set",
870            default="",
871            update=update_t_part
872            )
873
874    def insert_part(self, context, o, where):
875        self.manipulable_disable(context)
876        self.auto_update = False
877        # the part we do split
878        part_0 = self.parts[where]
879        part_0.length /= 2
880        part_0.da /= 2
881        self.parts.add()
882        part_1 = self.parts[len(self.parts) - 1]
883        part_1.type = part_0.type
884        part_1.length = part_0.length
885        part_1.da = part_0.da
886        part_1.a0 = 0
887        # move after current one
888        self.parts.move(len(self.parts) - 1, where + 1)
889        self.n_parts += 1
890        # re-eval childs location
891        g = self.get_generator()
892        self.setup_childs(o, g)
893
894        self.setup_manipulators()
895        self.auto_update = True
896
897    def add_part(self, context, length):
898        self.manipulable_disable(context)
899        self.auto_update = False
900        p = self.parts.add()
901        p.length = length
902        self.parts.move(len(self.parts) - 1, self.n_parts)
903        self.n_parts += 1
904        self.setup_manipulators()
905        self.auto_update = True
906        return self.parts[self.n_parts - 1]
907
908    def remove_part(self, context, o, where):
909        self.manipulable_disable(context)
910        self.auto_update = False
911        # preserve shape
912        # using generator
913        if where > 0:
914            g = self.get_generator()
915            w = g.segs[where - 1]
916            w.p1 = g.segs[where].p1
917
918            if where + 1 < self.n_parts:
919                self.parts[where + 1].a0 = g.segs[where + 1].delta_angle(w)
920
921            part = self.parts[where - 1]
922
923            if "C_" in part.type:
924                part.radius = w.r
925            else:
926                part.length = w.length
927
928            if where > 1:
929                part.a0 = w.delta_angle(g.segs[where - 2])
930            else:
931                part.a0 = w.straight(1, 0).angle
932
933        self.parts.remove(where)
934        self.n_parts -= 1
935
936        # re-eval child location
937        g = self.get_generator()
938        self.setup_childs(o, g)
939
940        # fix snap manipulators index
941        self.setup_manipulators()
942        self.auto_update = True
943
944    def get_generator(self):
945        # print("get_generator")
946        g = WallGenerator(self.parts)
947        for part in self.parts:
948            g.add_part(part, self.z, self.flip)
949        g.close(self.closed)
950        return g
951
952    def update_parts(self, o, update_childs=False):
953        # print("update_parts")
954        # remove rows
955        # NOTE:
956        # n_parts+1
957        # as last one is end point of last segment or closing one
958        row_change = False
959        for i in range(len(self.parts), self.n_parts + 1, -1):
960            row_change = True
961            self.parts.remove(i - 1)
962
963        # add rows
964        for i in range(len(self.parts), self.n_parts + 1):
965            row_change = True
966            self.parts.add()
967
968        self.setup_manipulators()
969
970        g = self.get_generator()
971
972        if o is not None and (row_change or update_childs):
973            self.setup_childs(o, g)
974
975        return g
976
977    def setup_manipulators(self):
978
979        if len(self.manipulators) == 0:
980            # make manipulators selectable
981            s = self.manipulators.add()
982            s.prop1_name = "width"
983            s = self.manipulators.add()
984            s.prop1_name = "n_parts"
985            s.type_key = 'COUNTER'
986            s = self.manipulators.add()
987            s.prop1_name = "z"
988            s.normal = (0, 1, 0)
989
990        if self.t_part != "" and len(self.manipulators) < 4:
991            s = self.manipulators.add()
992            s.prop1_name = "x"
993            s.type_key = 'DELTA_LOC'
994
995        for i in range(self.n_parts + 1):
996            p = self.parts[i]
997            n_manips = len(p.manipulators)
998            if n_manips < 1:
999                s = p.manipulators.add()
1000                s.type_key = "ANGLE"
1001                s.prop1_name = "a0"
1002            if n_manips < 2:
1003                s = p.manipulators.add()
1004                s.type_key = "SIZE"
1005                s.prop1_name = "length"
1006            if n_manips < 3:
1007                s = p.manipulators.add()
1008                s.type_key = 'WALL_SNAP'
1009                s.prop1_name = str(i)
1010                s.prop2_name = 'z'
1011            if n_manips < 4:
1012                s = p.manipulators.add()
1013                s.type_key = 'DUMB_STRING'
1014                s.prop1_name = str(i + 1)
1015            p.manipulators[2].prop1_name = str(i)
1016            p.manipulators[3].prop1_name = str(i + 1)
1017
1018    def interpolate_bezier(self, pts, wM, p0, p1, resolution):
1019        if resolution == 0:
1020            pts.append(wM @ p0.co.to_3d())
1021        else:
1022            v = (p1.co - p0.co).normalized()
1023            d1 = (p0.handle_right - p0.co).normalized()
1024            d2 = (p1.co - p1.handle_left).normalized()
1025            if d1 == v and d2 == v:
1026                pts.append(wM @ p0.co.to_3d())
1027            else:
1028                seg = interpolate_bezier(wM @ p0.co,
1029                    wM @ p0.handle_right,
1030                    wM @ p1.handle_left,
1031                    wM @ p1.co,
1032                    resolution + 1)
1033                for i in range(resolution):
1034                    pts.append(seg[i].to_3d())
1035
1036    def is_cw(self, pts):
1037        p0 = pts[0]
1038        d = 0
1039        for p in pts[1:]:
1040            d += (p.x * p0.y - p.y * p0.x)
1041            p0 = p
1042        return d > 0
1043
1044    def from_spline(self, wM, resolution, spline):
1045        pts = []
1046        if spline.type == 'POLY':
1047            pts = [wM @ p.co.to_3d() for p in spline.points]
1048            if spline.use_cyclic_u:
1049                pts.append(pts[0])
1050        elif spline.type == 'BEZIER':
1051            points = spline.bezier_points
1052            for i in range(1, len(points)):
1053                p0 = points[i - 1]
1054                p1 = points[i]
1055                self.interpolate_bezier(pts, wM, p0, p1, resolution)
1056            if spline.use_cyclic_u:
1057                p0 = points[-1]
1058                p1 = points[0]
1059                self.interpolate_bezier(pts, wM, p0, p1, resolution)
1060                pts.append(pts[0])
1061            else:
1062                pts.append(wM @ points[-1].co)
1063
1064        if self.is_cw(pts):
1065            pts = list(reversed(pts))
1066
1067        self.auto_update = False
1068        self.from_points(pts, spline.use_cyclic_u)
1069        self.auto_update = True
1070
1071    def from_points(self, pts, closed):
1072
1073        self.n_parts = len(pts) - 1
1074
1075        if closed:
1076            self.n_parts -= 1
1077
1078        self.update_parts(None)
1079
1080        p0 = pts.pop(0)
1081        a0 = 0
1082        for i, p1 in enumerate(pts):
1083            dp = p1 - p0
1084            da = atan2(dp.y, dp.x) - a0
1085            if da > pi:
1086                da -= 2 * pi
1087            if da < -pi:
1088                da += 2 * pi
1089            if i >= len(self.parts):
1090                print("Too many pts for parts")
1091                break
1092            p = self.parts[i]
1093            p.length = dp.to_2d().length
1094            p.dz = dp.z
1095            p.a0 = da
1096            a0 += da
1097            p0 = p1
1098
1099        self.closed = closed
1100
1101    def reverse(self, context, o):
1102
1103        g = self.get_generator()
1104
1105        self.auto_update = False
1106
1107        pts = [seg.p0.to_3d() for seg in g.segs]
1108
1109        if not self.closed:
1110            g.segs.pop()
1111
1112        g_segs = list(reversed(g.segs))
1113
1114        last_seg = None
1115
1116        for i, seg in enumerate(g_segs):
1117
1118            s = seg.oposite
1119            if "Curved" in type(seg).__name__:
1120                self.parts[i].type = "C_WALL"
1121                self.parts[i].radius = s.r
1122                self.parts[i].da = s.da
1123            else:
1124                self.parts[i].type = "S_WALL"
1125                self.parts[i].length = s.length
1126
1127            self.parts[i].a0 = s.delta_angle(last_seg)
1128
1129            last_seg = s
1130
1131        if self.closed:
1132            pts.append(pts[0])
1133
1134        pts = list(reversed(pts))
1135
1136        # location wont change for closed walls
1137        if not self.closed:
1138            dp = pts[0] - pts[-1]
1139            # pre-translate as dp is in local coordsys
1140            o.matrix_world = o.matrix_world @ Matrix([
1141                [1, 0, 0, dp.x],
1142                [0, 1, 0, dp.y],
1143                [0, 0, 1, 0],
1144                [0, 0, 0, 1],
1145                ])
1146
1147        # self.from_points(pts, self.closed)
1148
1149        g = self.get_generator()
1150
1151        self.setup_childs(o, g)
1152        self.auto_update = True
1153
1154        # flip does trigger relocate and keep childs orientation
1155        self.flip = not self.flip
1156
1157    def update(self, context, manipulable_refresh=False, update_childs=False):
1158
1159        o = self.find_in_selection(context, self.auto_update)
1160
1161        if o is None:
1162            return
1163
1164        if manipulable_refresh:
1165            # prevent crash by removing all manipulators refs to datablock before changes
1166            self.manipulable_disable(context)
1167
1168        verts = []
1169        faces = []
1170
1171        g = self.update_parts(o, update_childs)
1172        # print("make_wall")
1173        g.make_wall(self.step_angle, self.flip, self.closed, verts, faces)
1174
1175        if self.closed:
1176            f = len(verts)
1177            if self.flip:
1178                faces.append((0, f - 2, f - 1, 1))
1179            else:
1180                faces.append((f - 2, 0, 1, f - 1))
1181
1182        # print("buildmesh")
1183        bmed.buildmesh(context, o, verts, faces, matids=None, uvs=None, weld=True)
1184
1185        side = 1
1186        if self.flip:
1187            side = -1
1188        # Width
1189        offset = side * (0.5 * self.x_offset) * self.width
1190        self.manipulators[0].set_pts([
1191            g.segs[0].sized_normal(0, offset + 0.5 * side * self.width).v.to_3d(),
1192            g.segs[0].sized_normal(0, offset - 0.5 * side * self.width).v.to_3d(),
1193            (-side, 0, 0)
1194            ])
1195
1196        # Parts COUNTER
1197        self.manipulators[1].set_pts([g.segs[-2].lerp(1.1).to_3d(),
1198            g.segs[-2].lerp(1.1 + 0.5 / g.segs[-2].length).to_3d(),
1199            (-side, 0, 0)
1200            ])
1201
1202        # Height
1203        self.manipulators[2].set_pts([
1204            (0, 0, 0),
1205            (0, 0, self.z),
1206            (-1, 0, 0)
1207            ], normal=g.segs[0].straight(side, 0).v.to_3d())
1208
1209        if self.t_part != "":
1210            t = 0.3 / g.segs[0].length
1211            self.manipulators[3].set_pts([
1212                g.segs[0].sized_normal(t, 0.1).p1.to_3d(),
1213                g.segs[0].sized_normal(t, -0.1).p1.to_3d(),
1214                (1, 0, 0)
1215                ])
1216
1217        if self.realtime:
1218            # update child location and size
1219            self.relocate_childs(context, o, g)
1220            # store gl points
1221            self.update_childs(context, o, g)
1222        else:
1223            bpy.ops.archipack.wall2_throttle_update(name=o.name)
1224
1225        modif = o.modifiers.get('Wall')
1226        if modif is None:
1227            modif = o.modifiers.new('Wall', 'SOLIDIFY')
1228            modif.use_quality_normals = True
1229            modif.use_even_offset = True
1230            modif.material_offset_rim = 2
1231            modif.material_offset = 1
1232
1233        modif.thickness = self.width
1234        modif.offset = self.x_offset
1235
1236        if manipulable_refresh:
1237            # print("manipulable_refresh=True")
1238            self.manipulable_refresh = True
1239
1240        self.restore_context(context)
1241
1242    # manipulable children objects like windows and doors
1243    def child_partition(self, array, begin, end):
1244        pivot = begin
1245        for i in range(begin + 1, end + 1):
1246            # wall idx
1247            if array[i][1] < array[begin][1]:
1248                pivot += 1
1249                array[i], array[pivot] = array[pivot], array[i]
1250            # param t on the wall
1251            elif array[i][1] == array[begin][1] and array[i][4] <= array[begin][4]:
1252                pivot += 1
1253                array[i], array[pivot] = array[pivot], array[i]
1254        array[pivot], array[begin] = array[begin], array[pivot]
1255        return pivot
1256
1257    def sort_child(self, array, begin=0, end=None):
1258        # print("sort_child")
1259        if end is None:
1260            end = len(array) - 1
1261
1262        def _quicksort(array, begin, end):
1263            if begin >= end:
1264                return
1265            pivot = self.child_partition(array, begin, end)
1266            _quicksort(array, begin, pivot - 1)
1267            _quicksort(array, pivot + 1, end)
1268        return _quicksort(array, begin, end)
1269
1270    def add_child(self, name, wall_idx, pos, flip):
1271        # print("add_child %s %s" % (name, wall_idx))
1272        c = self.childs.add()
1273        c.child_name = name
1274        c.wall_idx = wall_idx
1275        c.pos = pos
1276        c.flip = flip
1277        m = c.manipulators.add()
1278        m.type_key = 'DELTA_LOC'
1279        m.prop1_name = "x"
1280        m = c.manipulators.add()
1281        m.type_key = 'SNAP_SIZE_LOC'
1282        m.prop1_name = "x"
1283        m.prop2_name = "x"
1284
1285    def setup_childs(self, o, g):
1286        """
1287            Store childs
1288            create manipulators
1289            call after a boolean oop
1290        """
1291        # tim = time.time()
1292        self.childs.clear()
1293        self.childs_manipulators.clear()
1294        if o.parent is None:
1295            return
1296        wall_with_childs = [0 for i in range(self.n_parts + 1)]
1297        relocate = []
1298        dmax = 2 * self.width
1299
1300        wtM = o.matrix_world
1301        wrM = Matrix([
1302            wtM[0].to_2d(),
1303            wtM[1].to_2d()
1304            ])
1305        witM = wtM.inverted()
1306
1307        for child in o.parent.children:
1308            # filter allowed childs
1309            cd = child.data
1310            wd = archipack_wall2.datablock(child)
1311            if (child != o and cd is not None and (
1312                    'archipack_window' in cd or
1313                    'archipack_door' in cd or (
1314                        wd is not None and
1315                        o.name in wd.t_part
1316                        )
1317                    )):
1318
1319                # setup on T linked walls
1320                if wd is not None:
1321                    wg = wd.get_generator()
1322                    wd.setup_childs(child, wg)
1323
1324                ctM = child.matrix_world
1325                crM = Matrix([
1326                    ctM[0].to_2d(),
1327                    ctM[1].to_2d()
1328                ])
1329
1330                # pt in w coordsys
1331                pos = ctM.translation
1332                pt = (witM @ pos).to_2d()
1333
1334                for wall_idx, wall in enumerate(g.segs):
1335                    # may be optimized with a bound check
1336                    res, dist, t = wall.point_sur_segment(pt)
1337                    # outside is on the right side of the wall
1338                    #  p1
1339                    #  |-- x
1340                    #  p0
1341                    if res and t > 0 and t < 1 and abs(dist) < dmax:
1342                        # dir in world coordsys
1343                        dir = wrM @ wall.sized_normal(t, 1).v
1344                        wall_with_childs[wall_idx] = 1
1345                        m = self.childs_manipulators.add()
1346                        m.type_key = 'DUMB_SIZE'
1347                        # always make window points outside
1348                        if "archipack_window" in cd:
1349                            flip = self.flip
1350                        else:
1351                            dir_y = crM @ Vector((0, -1))
1352                            # let door orient where user want
1353                            flip = (dir_y - dir).length > 0.5
1354                        # store z in wall space
1355                        relocate.append((
1356                            child.name,
1357                            wall_idx,
1358                            (t * wall.length, dist, (witM @ pos).z),
1359                            flip,
1360                            t))
1361                        break
1362
1363        self.sort_child(relocate)
1364        for child in relocate:
1365            name, wall_idx, pos, flip, t = child
1366            self.add_child(name, wall_idx, pos, flip)
1367
1368        # add a dumb size from last child to end of wall segment
1369        for i in range(sum(wall_with_childs)):
1370            m = self.childs_manipulators.add()
1371            m.type_key = 'DUMB_SIZE'
1372        # print("setup_childs:%1.4f" % (time.time()-tim))
1373
1374    def relocate_childs(self, context, o, g):
1375        """
1376            Move and resize childs after wall edition
1377        """
1378        # print("relocate_childs")
1379        # tim = time.time()
1380        w = -self.x_offset * self.width
1381        if self.flip:
1382            w = -w
1383        tM = o.matrix_world
1384        for child in self.childs:
1385            c, d = child.get_child(context)
1386            if c is None:
1387                continue
1388            t = child.pos.x / g.segs[child.wall_idx].length
1389            n = g.segs[child.wall_idx].sized_normal(t, 1)
1390            rx, ry = -n.v
1391            rx, ry = ry, -rx
1392            if child.flip:
1393                rx, ry = -rx, -ry
1394
1395            if d is not None:
1396                # print("change flip:%s width:%s" % (d.flip != child.flip, d.y != self.width))
1397                if d.y != self.width or d.flip != child.flip:
1398                    c.select_set(state=True)
1399                    d.auto_update = False
1400                    d.flip = child.flip
1401                    d.y = self.width
1402                    d.auto_update = True
1403                    c.select_set(state=False)
1404                x, y = n.p - (0.5 * w * n.v)
1405            else:
1406                x, y = n.p - (child.pos.y * n.v)
1407
1408            context.view_layer.objects.active = o
1409            # preTranslate
1410            c.matrix_world = tM @ Matrix([
1411                [rx, -ry, 0, x],
1412                [ry, rx, 0, y],
1413                [0, 0, 1, child.pos.z],
1414                [0, 0, 0, 1]
1415            ])
1416
1417            # Update T linked wall's childs
1418            if archipack_wall2.filter(c):
1419                d = archipack_wall2.datablock(c)
1420                cg = d.get_generator()
1421                d.relocate_childs(context, c, cg)
1422
1423        # print("relocate_childs:%1.4f" % (time.time()-tim))
1424
1425    def update_childs(self, context, o, g):
1426        """
1427            setup gl points for childs
1428        """
1429        # print("update_childs")
1430
1431        if o.parent is None:
1432            return
1433
1434        # swap manipulators so they always face outside
1435        manip_side = 1
1436        if self.flip:
1437            manip_side = -1
1438
1439        itM = o.matrix_world.inverted()
1440        m_idx = 0
1441        for wall_idx, wall in enumerate(g.segs):
1442            p0 = wall.lerp(0)
1443            wall_has_childs = False
1444            for child in self.childs:
1445                if child.wall_idx == wall_idx:
1446                    c, d = child.get_child(context)
1447                    if d is not None:
1448                        # child is either a window or a door
1449                        wall_has_childs = True
1450                        dt = 0.5 * d.x / wall.length
1451                        pt = (itM @ c.matrix_world.translation).to_2d()
1452                        res, y, t = wall.point_sur_segment(pt)
1453                        child.pos = (wall.length * t, y, child.pos.z)
1454                        p1 = wall.lerp(t - dt)
1455                        # dumb size between childs
1456                        self.childs_manipulators[m_idx].set_pts([
1457                            (p0.x, p0.y, 0),
1458                            (p1.x, p1.y, 0),
1459                            (manip_side * 0.5, 0, 0)])
1460                        m_idx += 1
1461                        x, y = 0.5 * d.x, -self.x_offset * 0.5 * d.y
1462
1463                        if child.flip:
1464                            side = -manip_side
1465                        else:
1466                            side = manip_side
1467
1468                        # delta loc
1469                        child.manipulators[0].set_pts([(-x, side * -y, 0), (x, side * -y, 0), (side, 0, 0)])
1470                        # loc size
1471                        child.manipulators[1].set_pts([
1472                            (-x, side * -y, 0),
1473                            (x, side * -y, 0),
1474                            (0.5 * side, 0, 0)])
1475                        p0 = wall.lerp(t + dt)
1476            p1 = wall.lerp(1)
1477            if wall_has_childs:
1478                # dub size after all childs
1479                self.childs_manipulators[m_idx].set_pts([
1480                    (p0.x, p0.y, 0),
1481                    (p1.x, p1.y, 0),
1482                    (manip_side * 0.5, 0, 0)])
1483                m_idx += 1
1484
1485    def manipulate_childs(self, context):
1486        """
1487            setup child manipulators
1488        """
1489        # print("manipulate_childs")
1490        n_parts = self.n_parts
1491        if self.closed:
1492            n_parts += 1
1493
1494        for wall_idx in range(n_parts):
1495            for child in self.childs:
1496                if child.wall_idx == wall_idx:
1497                    c, d = child.get_child(context)
1498                    if d is not None:
1499                        # delta loc
1500                        self.manip_stack.append(child.manipulators[0].setup(context, c, d, self.manipulate_callback))
1501                        # loc size
1502                        self.manip_stack.append(child.manipulators[1].setup(context, c, d, self.manipulate_callback))
1503
1504    def manipulate_callback(self, context, o=None, manipulator=None):
1505        found = False
1506        if o.parent is not None:
1507            for c in o.parent.children:
1508                if (archipack_wall2.datablock(c) == self):
1509                    context.view_layer.objects.active = c
1510                    found = True
1511                    break
1512        if found:
1513            self.manipulable_manipulate(context, manipulator=manipulator)
1514
1515    def manipulable_manipulate(self, context, event=None, manipulator=None):
1516        type_name = type(manipulator).__name__
1517        # print("manipulable_manipulate %s" % (type_name))
1518        if type_name in [
1519                'DeltaLocationManipulator',
1520                'SizeLocationManipulator',
1521                'SnapSizeLocationManipulator'
1522                ]:
1523            # update manipulators pos of childs
1524            o = context.active_object
1525            if o.parent is None:
1526                return
1527            g = self.get_generator()
1528            itM = o.matrix_world.inverted() @ o.parent.matrix_world
1529            for child in self.childs:
1530                c, d = child.get_child(context)
1531                if d is not None:
1532                    wall = g.segs[child.wall_idx]
1533                    pt = (itM @ c.location).to_2d()
1534                    res, d, t = wall.point_sur_segment(pt)
1535                    child.pos = (t * wall.length, d, child.pos.z)
1536            # update childs manipulators
1537            self.update_childs(context, o, g)
1538
1539    def manipulable_move_t_part(self, context, o=None, manipulator=None):
1540        """
1541            Callback for t_parts childs
1542        """
1543        type_name = type(manipulator).__name__
1544        # print("manipulable_manipulate %s" % (type_name))
1545        if type_name in [
1546                'DeltaLocationManipulator'
1547                ]:
1548            # update manipulators pos of childs
1549            if archipack_wall2.datablock(o) != self:
1550                return
1551            g = self.get_generator()
1552            # update childs
1553            self.relocate_childs(context, o, g)
1554
1555    def manipulable_release(self, context):
1556        """
1557            Override with action to do on mouse release
1558            eg: big update
1559        """
1560        return
1561
1562    def manipulable_setup(self, context):
1563        # print("manipulable_setup")
1564        self.manipulable_disable(context)
1565        o = context.active_object
1566
1567        # setup childs manipulators
1568        self.manipulate_childs(context)
1569        n_parts = self.n_parts
1570        if self.closed:
1571            n_parts += 1
1572
1573        # update manipulators on version change
1574        self.setup_manipulators()
1575
1576        for i, part in enumerate(self.parts):
1577
1578            if i < n_parts:
1579                if i > 0:
1580                    # start angle
1581                    self.manip_stack.append(part.manipulators[0].setup(context, o, part))
1582
1583                # length / radius + angle
1584                self.manip_stack.append(part.manipulators[1].setup(context, o, part))
1585                # segment index
1586                self.manip_stack.append(part.manipulators[3].setup(context, o, self))
1587
1588            # snap point
1589            self.manip_stack.append(part.manipulators[2].setup(context, o, self))
1590
1591        # height as per segment will be here when done
1592
1593        # width + counter
1594        for m in self.manipulators:
1595            self.manip_stack.append(m.setup(context, o, self, self.manipulable_move_t_part))
1596
1597        # dumb between childs
1598        for m in self.childs_manipulators:
1599            self.manip_stack.append(m.setup(context, o, self))
1600
1601    def manipulable_exit(self, context):
1602        """
1603            Override with action to do when modal exit
1604        """
1605        return
1606
1607    def manipulable_invoke(self, context):
1608        """
1609            call this in operator invoke()
1610        """
1611        # print("manipulable_invoke")
1612        if self.manipulate_mode:
1613            self.manipulable_disable(context)
1614            return False
1615
1616        # self.manip_stack = []
1617        o = context.active_object
1618        g = self.get_generator()
1619        # setup childs manipulators
1620        self.setup_childs(o, g)
1621        # store gl points
1622        self.update_childs(context, o, g)
1623        # don't do anything ..
1624        # self.manipulable_release(context)
1625        # self.manipulate_mode = True
1626        self.manipulable_setup(context)
1627        self.manipulate_mode = True
1628
1629        self._manipulable_invoke(context)
1630
1631        return True
1632
1633    def find_roof(self, context, o, g):
1634        tM = o.matrix_world
1635        up = Vector((0, 0, 1))
1636        for seg in g.segs:
1637            p = tM @ seg.p0.to_3d()
1638            p.z = 0.01
1639            # prevent self intersect
1640            o.hide_viewport = True
1641            res, pos, normal, face_index, r, matrix_world = context.scene.ray_cast(
1642                depsgraph=context.view_layer.depsgraph,
1643                origin=p,
1644                direction=up)
1645
1646            o.hide_viewport = False
1647            # print("res:%s" % res)
1648            if res and r.data is not None and "archipack_roof" in r.data:
1649                return r, r.data.archipack_roof[0]
1650
1651        return None, None
1652
1653
1654# Update throttle (hack)
1655# use 2 globals to store a timer and state of update_action
1656# Use to update floor boolean on edit
1657update_timer = None
1658update_timer_updating = False
1659throttle_delay = 0.5
1660throttle_start = 0
1661
1662
1663class ARCHIPACK_OT_wall2_throttle_update(Operator):
1664    bl_idname = "archipack.wall2_throttle_update"
1665    bl_label = "Update childs with a delay"
1666
1667    name : StringProperty()
1668
1669    def modal(self, context, event):
1670        global update_timer_updating
1671        if event.type == 'TIMER' and not update_timer_updating:
1672            # can't rely on TIMER event as another timer may run
1673            if time.time() - throttle_start > throttle_delay:
1674                update_timer_updating = True
1675                o = context.scene.objects.get(self.name.strip())
1676                if o is not None:
1677                    m = o.modifiers.get("AutoBoolean")
1678                    if m is not None:
1679                        o.hide_viewport = False
1680                        # o.display_type = 'TEXTURED'
1681                        # m.show_viewport = True
1682
1683                    return self.cancel(context)
1684        return {'PASS_THROUGH'}
1685
1686    def execute(self, context):
1687        global update_timer
1688        global update_timer_updating
1689        global throttle_delay
1690        global throttle_start
1691        if update_timer is not None:
1692            context.window_manager.event_timer_remove(update_timer)
1693            if update_timer_updating:
1694                return {'CANCELLED'}
1695            # reset update_timer so it only occurs once 0.1s after last action
1696            throttle_start = time.time()
1697            update_timer = context.window_manager.event_timer_add(throttle_delay, context.window)
1698            return {'CANCELLED'}
1699        throttle_start = time.time()
1700        update_timer_updating = False
1701        context.window_manager.modal_handler_add(self)
1702        update_timer = context.window_manager.event_timer_add(throttle_delay, context.window)
1703        return {'RUNNING_MODAL'}
1704
1705    def cancel(self, context):
1706        global update_timer
1707        context.window_manager.event_timer_remove(update_timer)
1708        update_timer = None
1709        return {'CANCELLED'}
1710
1711
1712class ARCHIPACK_PT_wall2(Panel):
1713    bl_idname = "ARCHIPACK_PT_wall2"
1714    bl_label = "Wall"
1715    bl_space_type = 'VIEW_3D'
1716    bl_region_type = 'UI'
1717    bl_category = 'Archipack'
1718
1719    def draw(self, context):
1720        prop = archipack_wall2.datablock(context.object)
1721        if prop is None:
1722            return
1723        layout = self.layout
1724        row = layout.row(align=True)
1725        row.operator("archipack.wall2_manipulate", icon='VIEW_PAN')
1726        # row = layout.row(align=True)
1727        # row.prop(prop, 'realtime')
1728        box = layout.box()
1729        box.prop(prop, 'n_parts')
1730        box.prop(prop, 'step_angle')
1731        box.prop(prop, 'width')
1732        box.prop(prop, 'z')
1733        box.prop(prop, 'flip')
1734        box.prop(prop, 'x_offset')
1735        row = layout.row()
1736        row.prop(prop, "closed")
1737        row = layout.row()
1738        row.prop_search(prop, "t_part", context.scene, "objects", text="T link", icon='OBJECT_DATAMODE')
1739        layout.operator("archipack.wall2_reverse", icon='FILE_REFRESH')
1740        row = layout.row(align=True)
1741        row.operator("archipack.wall2_fit_roof")
1742        # row.operator("archipack.wall2_fit_roof", text="Inside").inside = True
1743        n_parts = prop.n_parts
1744        if prop.closed:
1745            n_parts += 1
1746        for i, part in enumerate(prop.parts):
1747            if i < n_parts:
1748                box = layout.box()
1749                part.draw(box, context, i)
1750
1751    @classmethod
1752    def poll(cls, context):
1753        return archipack_wall2.filter(context.active_object)
1754
1755
1756# ------------------------------------------------------------------
1757# Define operator class to create object
1758# ------------------------------------------------------------------
1759
1760
1761class ARCHIPACK_OT_wall2(ArchipackCreateTool, Operator):
1762    bl_idname = "archipack.wall2"
1763    bl_label = "Wall"
1764    bl_description = "Create a Wall"
1765    bl_category = 'Archipack'
1766    bl_options = {'REGISTER', 'UNDO'}
1767
1768    def create(self, context):
1769        m = bpy.data.meshes.new("Wall")
1770        o = bpy.data.objects.new("Wall", m)
1771        d = m.archipack_wall2.add()
1772        d.manipulable_selectable = True
1773        self.link_object_to_scene(context, o)
1774        o.select_set(state=True)
1775        # around 12 degree
1776        m.auto_smooth_angle = 0.20944
1777        context.view_layer.objects.active = o
1778        self.load_preset(d)
1779        self.add_material(o)
1780        return o
1781
1782    def execute(self, context):
1783        if context.mode == "OBJECT":
1784            bpy.ops.object.select_all(action="DESELECT")
1785            o = self.create(context)
1786            o.location = bpy.context.scene.cursor.location
1787            o.select_set(state=True)
1788            context.view_layer.objects.active = o
1789            self.manipulate()
1790            return {'FINISHED'}
1791        else:
1792            self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
1793            return {'CANCELLED'}
1794
1795
1796class ARCHIPACK_OT_wall2_from_curve(Operator):
1797    bl_idname = "archipack.wall2_from_curve"
1798    bl_label = "Wall curve"
1799    bl_description = "Create a wall from a curve"
1800    bl_category = 'Archipack'
1801    bl_options = {'REGISTER', 'UNDO'}
1802
1803    auto_manipulate : BoolProperty(default=True)
1804
1805    @classmethod
1806    def poll(self, context):
1807        return context.active_object is not None and context.active_object.type == 'CURVE'
1808
1809    def create(self, context):
1810        curve = context.active_object
1811        for spline in curve.data.splines:
1812            bpy.ops.archipack.wall2(auto_manipulate=self.auto_manipulate)
1813            o = context.view_layer.objects.active
1814            d = archipack_wall2.datablock(o)
1815            d.from_spline(curve.matrix_world, 12, spline)
1816            if spline.type == 'POLY':
1817                pt = spline.points[0].co
1818            elif spline.type == 'BEZIER':
1819                pt = spline.bezier_points[0].co
1820            else:
1821                pt = Vector((0, 0, 0))
1822            # pretranslate
1823            o.matrix_world = curve.matrix_world @ Matrix([
1824                [1, 0, 0, pt.x],
1825                [0, 1, 0, pt.y],
1826                [0, 0, 1, pt.z],
1827                [0, 0, 0, 1]
1828                ])
1829        return o
1830
1831    # -----------------------------------------------------
1832    # Execute
1833    # -----------------------------------------------------
1834    def execute(self, context):
1835        if context.mode == "OBJECT":
1836            bpy.ops.object.select_all(action="DESELECT")
1837            o = self.create(context)
1838            if o is not None:
1839                o.select_set(state=True)
1840                context.view_layer.objects.active = o
1841            return {'FINISHED'}
1842        else:
1843            self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
1844            return {'CANCELLED'}
1845
1846
1847class ARCHIPACK_OT_wall2_from_slab(Operator):
1848    bl_idname = "archipack.wall2_from_slab"
1849    bl_label = "->Wall"
1850    bl_description = "Create a wall from a slab"
1851    bl_category = 'Archipack'
1852    bl_options = {'REGISTER', 'UNDO'}
1853
1854    auto_manipulate : BoolProperty(default=True)
1855
1856    @classmethod
1857    def poll(self, context):
1858        o = context.active_object
1859        return o is not None and o.data is not None and 'archipack_slab' in o.data
1860
1861    def create(self, context):
1862        slab = context.active_object
1863        wd = slab.data.archipack_slab[0]
1864        bpy.ops.archipack.wall2(auto_manipulate=self.auto_manipulate)
1865        o = context.view_layer.objects.active
1866        d = archipack_wall2.datablock(o)
1867        d.auto_update = False
1868        d.parts.clear()
1869        d.n_parts = wd.n_parts - 1
1870        d.closed = True
1871        for part in wd.parts:
1872            p = d.parts.add()
1873            if "S_" in part.type:
1874                p.type = "S_WALL"
1875            else:
1876                p.type = "C_WALL"
1877            p.length = part.length
1878            p.radius = part.radius
1879            p.da = part.da
1880            p.a0 = part.a0
1881        o.select_set(state=True)
1882        context.view_layer.objects.active = o
1883        d.auto_update = True
1884        # pretranslate
1885        o.matrix_world = slab.matrix_world.copy()
1886
1887        bpy.ops.object.select_all(action='DESELECT')
1888        # parenting childs to wall reference point
1889        if o.parent is None:
1890            x, y, z = o.bound_box[0]
1891            context.scene.cursor.location = o.matrix_world @ Vector((x, y, z))
1892            # fix issue #9
1893            context.view_layer.objects.active = o
1894            bpy.ops.archipack.reference_point()
1895        else:
1896            o.parent.select_set(state=True)
1897            context.view_layer.objects.active = o.parent
1898        o.select_set(state=True)
1899        slab.select_set(state=True)
1900        bpy.ops.archipack.parent_to_reference()
1901        o.parent.select_set(state=False)
1902        return o
1903
1904    # -----------------------------------------------------
1905    # Execute
1906    # -----------------------------------------------------
1907    def execute(self, context):
1908        if context.mode == "OBJECT":
1909            bpy.ops.object.select_all(action="DESELECT")
1910            o = self.create(context)
1911            o.select_set(state=True)
1912            context.view_layer.objects.active = o
1913            return {'FINISHED'}
1914        else:
1915            self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
1916            return {'CANCELLED'}
1917
1918
1919class ARCHIPACK_OT_wall2_fit_roof(Operator):
1920    bl_idname = "archipack.wall2_fit_roof"
1921    bl_label = "Fit roof"
1922    bl_description = "Fit roof"
1923    bl_category = 'Archipack'
1924    bl_options = {'REGISTER', 'UNDO'}
1925
1926    inside : BoolProperty(default=False)
1927
1928    @classmethod
1929    def poll(self, context):
1930        return archipack_wall2.filter(context.active_object)
1931
1932    def execute(self, context):
1933        o = context.active_object
1934        d = archipack_wall2.datablock(o)
1935        g = d.get_generator()
1936        r, rd = d.find_roof(context, o, g)
1937        if rd is not None:
1938            d.setup_childs(o, g)
1939            rd.make_wall_fit(context, r, o, self.inside)
1940        return {'FINISHED'}
1941
1942# ------------------------------------------------------------------
1943# Define operator class to draw a wall
1944# ------------------------------------------------------------------
1945
1946
1947class ARCHIPACK_OT_wall2_draw(ArchipackDrawTool, Operator):
1948    bl_idname = "archipack.wall2_draw"
1949    bl_label = "Draw a Wall"
1950    bl_description = "Create a wall by drawing its baseline in 3D view"
1951    bl_category = 'Archipack'
1952
1953    o = None
1954    state = 'RUNNING'
1955    flag_create = False
1956    flag_next = False
1957    wall_part1 = None
1958    wall_line1 = None
1959    line = None
1960    label = None
1961    feedback = None
1962    takeloc = Vector((0, 0, 0))
1963    sel = []
1964    act = None
1965
1966    # constraint to other wall and make a T child
1967    parent = None
1968    takemat = None
1969
1970    @classmethod
1971    def poll(cls, context):
1972        return True
1973
1974    def draw_callback(self, _self, context):
1975        self.feedback.draw(context)
1976
1977    def sp_draw(self, sp, context):
1978        z = 2.7
1979        if self.state == 'CREATE':
1980            p0 = self.takeloc
1981        else:
1982            p0 = sp.takeloc
1983
1984        p1 = sp.placeloc
1985        delta = p1 - p0
1986        # print("sp_draw state:%s delta:%s p0:%s p1:%s" % (self.state, delta.length, p0, p1))
1987        if delta.length == 0:
1988            return
1989        self.wall_part1.set_pos([p0, p1, Vector((p1.x, p1.y, p1.z + z)), Vector((p0.x, p0.y, p0.z + z))])
1990        self.wall_line1.set_pos([p0, p1, Vector((p1.x, p1.y, p1.z + z)), Vector((p0.x, p0.y, p0.z + z))])
1991        self.wall_part1.draw(context)
1992        self.wall_line1.draw(context)
1993        self.line.p = p0
1994        self.line.v = delta
1995        self.label.set_pos(context, self.line.length, self.line.lerp(0.5), self.line.v, normal=Vector((0, 0, 1)))
1996        self.label.draw(context)
1997        self.line.draw(context)
1998
1999    def sp_callback(self, context, event, state, sp):
2000        logger.debug("ARCHIPACK_OT_wall2_draw.sp_callback event %s %s state:%s", event.type, event.value, state)
2001
2002        if state == 'SUCCESS':
2003
2004            if self.state == 'CREATE':
2005                takeloc = self.takeloc
2006                delta = sp.placeloc - self.takeloc
2007            else:
2008                takeloc = sp.takeloc
2009                delta = sp.delta
2010
2011            old = context.object
2012            if self.o is None:
2013                bpy.ops.archipack.wall2(auto_manipulate=False)
2014                o = context.object
2015                o.location = takeloc
2016                self.o = o
2017                d = archipack_wall2.datablock(o)
2018
2019                part = d.parts[0]
2020                part.length = delta.length
2021            else:
2022                o = self.o
2023                # select and make active
2024                o.select_set(state=True)
2025                context.view_layer.objects.active = o
2026                d = archipack_wall2.datablock(o)
2027                # Check for end close to start and close when applicable
2028                dp = sp.placeloc - o.location
2029                if dp.length < 0.01:
2030                    d.closed = True
2031                    self.state = 'CANCEL'
2032                    return
2033
2034                part = d.add_part(context, delta.length)
2035
2036            # print("self.o :%s" % o.name)
2037            rM = o.matrix_world.inverted().to_3x3()
2038            g = d.get_generator()
2039            w = g.segs[-2]
2040            dp = rM @ delta
2041            da = atan2(dp.y, dp.x) - w.straight(1).angle
2042            a0 = part.a0 + da
2043            if a0 > pi:
2044                a0 -= 2 * pi
2045            if a0 < -pi:
2046                a0 += 2 * pi
2047            part.a0 = a0
2048            d.update(context)
2049
2050            old.select_set(state=True)
2051            context.view_layer.objects.active = old
2052            self.flag_next = True
2053            context.area.tag_redraw()
2054            # print("feedback.on:%s" % self.feedback.on)
2055
2056        self.state = state
2057
2058    def sp_init(self, context, event, state, sp):
2059        # print("sp_init event %s %s %s" % (event.type, event.value, state))
2060        if state == 'SUCCESS':
2061            # point placed, check if a wall was under mouse
2062            res, tM, wall, width, y, z_offset = self.mouse_hover_wall(context, event)
2063            if res:
2064                d = archipack_wall2.datablock(wall)
2065                if event.ctrl:
2066                    # user snap, use direction as constraint
2067                    tM.translation = sp.placeloc.copy()
2068                else:
2069                    # without snap, use wall's bottom
2070                    tM.translation -= y.normalized() * (0.5 * d.width)
2071                self.takeloc = tM.translation
2072                self.parent = wall.name
2073                self.takemat = tM
2074            else:
2075                self.takeloc = sp.placeloc.copy()
2076
2077            self.state = 'RUNNING'
2078            # print("feedback.on:%s" % self.feedback.on)
2079        elif state == 'CANCEL':
2080            self.state = state
2081            return
2082
2083    def ensure_ccw(self):
2084        """
2085            Wall to slab expect wall vertex order to be ccw
2086            so reverse order here when needed
2087        """
2088        d = archipack_wall2.datablock(self.o)
2089        g = d.get_generator(axis=False)
2090        pts = [seg.p0 for seg in g.segs]
2091
2092        if d.closed:
2093            pts.append(pts[0])
2094
2095        if d.is_cw(pts):
2096            d.x_offset = 1
2097            pts = list(reversed(pts))
2098            self.o.location += pts[0] - pts[-1]
2099
2100        d.from_points(pts, d.closed)
2101
2102    def modal(self, context, event):
2103
2104        context.area.tag_redraw()
2105        if event.type in {'NONE', 'TIMER', 'TIMER_REPORT', 'EVT_TWEAK_L', 'WINDOW_DEACTIVATE'}:
2106            return {'PASS_THROUGH'}
2107
2108        if self.keymap.check(event, self.keymap.delete):
2109            self.feedback.disable()
2110            bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
2111            self.o = None
2112            return {'FINISHED', 'PASS_THROUGH'}
2113
2114        if self.state == 'STARTING' and event.type not in {'ESC', 'RIGHTMOUSE'}:
2115            # wait for takeloc being visible when button is over horizon
2116            takeloc = self.mouse_to_plane(context, event)
2117            if takeloc is not None:
2118                logger.debug("ARCHIPACK_OT_wall2_draw.modal(STARTING) location:%s", takeloc)
2119                snap_point(takeloc=takeloc,
2120                           callback=self.sp_init,
2121                           constraint_axis=(True, True, False),
2122                           release_confirm=True)
2123            return {'RUNNING_MODAL'}
2124
2125        elif self.state == 'RUNNING':
2126            # print("RUNNING")
2127            logger.debug("ARCHIPACK_OT_wall2_draw.modal(RUNNING) location:%s", self.takeloc)
2128            self.state = 'CREATE'
2129            snap_point(takeloc=self.takeloc,
2130                       draw=self.sp_draw,
2131                       takemat=self.takemat,
2132                       transform_orientation=context.scene.transform_orientation_slots[0].type,
2133                       callback=self.sp_callback,
2134                       constraint_axis=(True, True, False),
2135                       release_confirm=self.max_style_draw_tool)
2136            return {'RUNNING_MODAL'}
2137
2138        elif self.state != 'CANCEL' and event.type in {'C', 'c'}:
2139
2140            logger.debug("ARCHIPACK_OT_wall2_draw.modal(%s) C pressed", self.state)
2141            self.feedback.disable()
2142            bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
2143
2144            o = self.o
2145            # select and make active
2146            o.select_set(state=True)
2147            context.view_layer.objects.active = o
2148
2149            d = archipack_wall2.datablock(o)
2150            d.closed = True
2151
2152            if bpy.ops.archipack.manipulate.poll():
2153                bpy.ops.archipack.manipulate('INVOKE_DEFAULT')
2154
2155            return {'FINISHED'}
2156
2157        elif self.state != 'CANCEL' and event.type in {'LEFTMOUSE', 'RET', 'NUMPAD_ENTER', 'SPACE'}:
2158
2159            # print('LEFTMOUSE %s' % (event.value))
2160            self.feedback.instructions(context, "Draw a wall", "Click & Drag to add a segment", [
2161                ('ENTER', 'Add part'),
2162                ('BACK_SPACE', 'Remove part'),
2163                ('CTRL', 'Snap'),
2164                ('C', 'Close wall and exit'),
2165                ('MMBTN', 'Constraint to axis'),
2166                ('X Y', 'Constraint to axis'),
2167                ('RIGHTCLICK or ESC', 'exit')
2168            ])
2169
2170            # press with max mode release with blender mode
2171            if self.max_style_draw_tool:
2172                evt_value = 'PRESS'
2173            else:
2174                evt_value = 'RELEASE'
2175
2176            if event.value == evt_value:
2177
2178                if self.flag_next:
2179                    self.flag_next = False
2180                    o = self.o
2181
2182                    # select and make active
2183                    o.select_set(state=True)
2184                    context.view_layer.objects.active = o
2185
2186                    d = archipack_wall2.datablock(o)
2187                    g = d.get_generator()
2188                    p0 = g.segs[-2].p0
2189                    p1 = g.segs[-2].p1
2190                    dp = p1 - p0
2191                    takemat = o.matrix_world @ Matrix([
2192                        [dp.x, dp.y, 0, p1.x],
2193                        [dp.y, -dp.x, 0, p1.y],
2194                        [0, 0, 1, 0],
2195                        [0, 0, 0, 1]
2196                    ])
2197                    takeloc = o.matrix_world @ p1.to_3d()
2198                    o.select_set(state=False)
2199                else:
2200                    takemat = None
2201                    takeloc = self.mouse_to_plane(context, event)
2202
2203                if takeloc is not None:
2204                    logger.debug("ARCHIPACK_OT_wall2_draw.modal(CREATE) location:%s", takeloc)
2205
2206                    snap_point(takeloc=takeloc,
2207                               takemat=takemat,
2208                               draw=self.sp_draw,
2209                               callback=self.sp_callback,
2210                               constraint_axis=(True, True, False),
2211                               release_confirm=self.max_style_draw_tool)
2212
2213            return {'RUNNING_MODAL'}
2214
2215        if self.keymap.check(event, self.keymap.undo) or (
2216                event.type in {'BACK_SPACE'} and event.value == 'RELEASE'
2217        ):
2218            if self.o is not None:
2219                o = self.o
2220
2221                # select and make active
2222                o.select_set(state=True)
2223                context.view_layer.objects.active = o
2224                d = archipack_wall2.datablock(o)
2225                if d.n_parts > 1:
2226                    d.n_parts -= 1
2227            return {'RUNNING_MODAL'}
2228
2229        if self.state == 'CANCEL' or (event.type in {'ESC', 'RIGHTMOUSE'} and
2230                                      event.value == 'RELEASE'):
2231
2232            self.feedback.disable()
2233            bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
2234            logger.debug("ARCHIPACK_OT_wall2_draw.modal(CANCEL) %s", event.type)
2235            if self.o is None:
2236                for o in self.sel:
2237                    o.select_set(state=True)
2238                # select and make active
2239                if self.act is not None:
2240                    self.act.select_set(state=True)
2241                    context.view_layer.objects.active = self.act
2242
2243            else:
2244                o = self.o
2245                o.select_set(state=True)
2246                context.view_layer.objects.active = o
2247
2248                # remove last segment with blender mode
2249                d = archipack_wall2.datablock(o)
2250                if not self.max_style_draw_tool:
2251                    if not d.closed and d.n_parts > 1:
2252                        d.n_parts -= 1
2253                o.select_set(state=True)
2254                context.view_layer.objects.active = o
2255                # make T child
2256                if self.parent is not None:
2257                    d.t_part = self.parent
2258
2259                if bpy.ops.archipack.manipulate.poll():
2260                    bpy.ops.archipack.manipulate('INVOKE_DEFAULT')
2261
2262            return {'FINISHED'}
2263
2264        return {'PASS_THROUGH'}
2265
2266    def invoke(self, context, event):
2267
2268        if context.mode == "OBJECT":
2269            prefs = context.preferences.addons[__name__.split('.')[0]].preferences
2270            self.max_style_draw_tool = prefs.max_style_draw_tool
2271            self.keymap = Keymaps(context)
2272            self.wall_part1 = GlPolygon((0.5, 0, 0, 0.2))
2273            self.wall_line1 = GlPolyline((0.5, 0, 0, 0.8))
2274            self.line = GlLine()
2275            self.label = GlText()
2276            self.feedback = FeedbackPanel()
2277            self.feedback.instructions(context, "Draw a wall", "Click & Drag to start", [
2278                ('CTRL', 'Snap'),
2279                ('MMBTN', 'Constraint to axis'),
2280                ('X Y', 'Constraint to axis'),
2281                ('SHIFT+CTRL+TAB', 'Switch snap mode'),
2282                ('RIGHTCLICK or ESC', 'exit without change')
2283            ])
2284            self.feedback.enable()
2285            args = (self, context)
2286
2287            self.sel = context.selected_objects[:]
2288            self.act = context.active_object
2289            bpy.ops.object.select_all(action="DESELECT")
2290
2291            self.state = 'STARTING'
2292
2293            self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback, args, 'WINDOW', 'POST_PIXEL')
2294            context.window_manager.modal_handler_add(self)
2295            return {'RUNNING_MODAL'}
2296        else:
2297            self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
2298            return {'CANCELLED'}
2299
2300
2301# ------------------------------------------------------------------
2302# Define operator class to manage parts
2303# ------------------------------------------------------------------
2304
2305
2306class ARCHIPACK_OT_wall2_insert(Operator):
2307    bl_idname = "archipack.wall2_insert"
2308    bl_label = "Insert"
2309    bl_description = "Insert part"
2310    bl_category = 'Archipack'
2311    bl_options = {'REGISTER', 'UNDO'}
2312    index : IntProperty(default=0)
2313
2314    def execute(self, context):
2315        if context.mode == "OBJECT":
2316            o = context.active_object
2317            d = archipack_wall2.datablock(o)
2318            if d is None:
2319                return {'CANCELLED'}
2320            d.insert_part(context, o, self.index)
2321            return {'FINISHED'}
2322        else:
2323            self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
2324            return {'CANCELLED'}
2325
2326
2327class ARCHIPACK_OT_wall2_remove(Operator):
2328    bl_idname = "archipack.wall2_remove"
2329    bl_label = "Remove"
2330    bl_description = "Remove part"
2331    bl_category = 'Archipack'
2332    bl_options = {'REGISTER', 'UNDO'}
2333    index : IntProperty(default=0)
2334
2335    def execute(self, context):
2336        if context.mode == "OBJECT":
2337            o = context.active_object
2338            d = archipack_wall2.datablock(o)
2339            if d is None:
2340                return {'CANCELLED'}
2341            d.remove_part(context, o, self.index)
2342            return {'FINISHED'}
2343        else:
2344            self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
2345            return {'CANCELLED'}
2346
2347
2348class ARCHIPACK_OT_wall2_reverse(Operator):
2349    bl_idname = "archipack.wall2_reverse"
2350    bl_label = "Reverse"
2351    bl_description = "Reverse parts order"
2352    bl_category = 'Archipack'
2353    bl_options = {'REGISTER', 'UNDO'}
2354
2355    def execute(self, context):
2356        if context.mode == "OBJECT":
2357            o = context.active_object
2358            d = archipack_wall2.datablock(o)
2359            if d is None:
2360                return {'CANCELLED'}
2361            d.reverse(context, o)
2362            return {'FINISHED'}
2363        else:
2364            self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
2365            return {'CANCELLED'}
2366
2367
2368# ------------------------------------------------------------------
2369# Define operator class to manipulate object
2370# ------------------------------------------------------------------
2371
2372
2373class ARCHIPACK_OT_wall2_manipulate(Operator):
2374    bl_idname = "archipack.wall2_manipulate"
2375    bl_label = "Manipulate"
2376    bl_description = "Manipulate"
2377    bl_options = {'REGISTER', 'UNDO'}
2378
2379    @classmethod
2380    def poll(self, context):
2381        return archipack_wall2.filter(context.active_object)
2382
2383    def invoke(self, context, event):
2384        d = archipack_wall2.datablock(context.active_object)
2385        d.manipulable_invoke(context)
2386        return {'FINISHED'}
2387
2388    def execute(self, context):
2389        """
2390            For use in boolean ops
2391        """
2392        if archipack_wall2.filter(context.active_object):
2393            o = context.active_object
2394            d = archipack_wall2.datablock(o)
2395            g = d.get_generator()
2396            d.setup_childs(o, g)
2397            d.update_childs(context, o, g)
2398            d.update(context)
2399            o.select_set(state=True)
2400            context.view_layer.objects.active = o
2401        return {'FINISHED'}
2402
2403
2404def register():
2405    bpy.utils.register_class(archipack_wall2_part)
2406    bpy.utils.register_class(archipack_wall2_child)
2407    bpy.utils.register_class(archipack_wall2)
2408    Mesh.archipack_wall2 = CollectionProperty(type=archipack_wall2)
2409    bpy.utils.register_class(ARCHIPACK_PT_wall2)
2410    bpy.utils.register_class(ARCHIPACK_OT_wall2)
2411    bpy.utils.register_class(ARCHIPACK_OT_wall2_draw)
2412    bpy.utils.register_class(ARCHIPACK_OT_wall2_insert)
2413    bpy.utils.register_class(ARCHIPACK_OT_wall2_remove)
2414    bpy.utils.register_class(ARCHIPACK_OT_wall2_reverse)
2415    bpy.utils.register_class(ARCHIPACK_OT_wall2_manipulate)
2416    bpy.utils.register_class(ARCHIPACK_OT_wall2_from_curve)
2417    bpy.utils.register_class(ARCHIPACK_OT_wall2_from_slab)
2418    bpy.utils.register_class(ARCHIPACK_OT_wall2_throttle_update)
2419    bpy.utils.register_class(ARCHIPACK_OT_wall2_fit_roof)
2420
2421
2422def unregister():
2423    bpy.utils.unregister_class(archipack_wall2_part)
2424    bpy.utils.unregister_class(archipack_wall2_child)
2425    bpy.utils.unregister_class(archipack_wall2)
2426    del Mesh.archipack_wall2
2427    bpy.utils.unregister_class(ARCHIPACK_PT_wall2)
2428    bpy.utils.unregister_class(ARCHIPACK_OT_wall2)
2429    bpy.utils.unregister_class(ARCHIPACK_OT_wall2_draw)
2430    bpy.utils.unregister_class(ARCHIPACK_OT_wall2_insert)
2431    bpy.utils.unregister_class(ARCHIPACK_OT_wall2_remove)
2432    bpy.utils.unregister_class(ARCHIPACK_OT_wall2_reverse)
2433    bpy.utils.unregister_class(ARCHIPACK_OT_wall2_manipulate)
2434    bpy.utils.unregister_class(ARCHIPACK_OT_wall2_from_curve)
2435    bpy.utils.unregister_class(ARCHIPACK_OT_wall2_from_slab)
2436    bpy.utils.unregister_class(ARCHIPACK_OT_wall2_throttle_update)
2437    bpy.utils.unregister_class(ARCHIPACK_OT_wall2_fit_roof)
2438