1# ##### BEGIN GPL LICENSE BLOCK #####
2#
3#  This program is free software; you can redistribute it and/or
4#  modify it under the terms of the GNU General Public License
5#  as published by the Free Software Foundation; either version 2
6#  of the License, or (at your option) any later version.
7#
8#  This program is distributed in the hope that it will be useful,
9#  but WITHOUT ANY WARRANTY; without even the implied warranty of
10#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11#  GNU General Public License for more details.
12#
13#  You should have received a copy of the GNU General Public License
14#  along with this program; if not, write to the Free Software Foundation,
15#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16#
17# ##### END GPL LICENSE BLOCK #####
18
19# <pep8 compliant>
20
21# ----------------------------------------------------------
22# Automatic generation of rooms
23# Author: Antonio Vazquez (antonioya) and Eduardo Gutierrez
24#
25# ----------------------------------------------------------
26# noinspection PyUnresolvedReferences
27import bpy
28from math import sin, cos, fabs, radians
29from mathutils import Vector
30from datetime import datetime
31from time import time
32from os import path
33from bpy.types import Operator, PropertyGroup, Object, Panel
34from bpy.props import StringProperty, FloatProperty, BoolProperty, IntProperty, FloatVectorProperty, \
35    CollectionProperty, EnumProperty
36from bpy_extras.io_utils import ExportHelper, ImportHelper
37from .achm_tools import *
38
39
40# ----------------------------------------------------------
41# Export menu UI
42# ----------------------------------------------------------
43class ARCHIMESH_OT_ExportRoom(Operator, ExportHelper):
44    bl_idname = "io_export.roomdata"
45    bl_description = 'Export Room data (.dat)'
46    bl_category = 'View'
47    bl_label = "Export"
48
49    # From ExportHelper. Filter filenames.
50    filename_ext = ".dat"
51    filter_glob: StringProperty(
52            default="*.dat",
53            options={'HIDDEN'},
54            )
55
56    filepath: StringProperty(
57            name="File Path",
58            description="File path used for exporting room data file",
59            maxlen=1024, default="",
60            )
61
62    # ----------------------------------------------------------
63    # Execute
64    # ----------------------------------------------------------
65    # noinspection PyUnusedLocal
66    def execute(self, context):
67        print("Exporting:", self.properties.filepath)
68        # noinspection PyBroadException
69        try:
70            myobj = bpy.context.active_object
71            mydata = myobj.RoomGenerator[0]
72
73            # -------------------------------
74            # extract path and filename
75            # -------------------------------
76            (filepath, filename) = path.split(self.properties.filepath)
77            print('Exporting %s' % filename)
78            # -------------------------------
79            # Open output file
80            # -------------------------------
81            realpath = path.realpath(path.expanduser(self.properties.filepath))
82            fout = open(realpath, 'w')
83
84            st = datetime.fromtimestamp(time()).strftime('%Y-%m-%d %H:%M:%S')
85            fout.write("# Archimesh room export data\n")
86            fout.write("# " + st + "\n")
87            fout.write("#======================================================\n")
88
89            fout.write("name=" + myobj.name + "\n")
90            fout.write("height=" + str(round(mydata.room_height, 3)) + "\n")
91            fout.write("thickness=" + str(round(mydata.wall_width, 3)) + "\n")
92            fout.write("inverse=" + str(mydata.inverse) + "\n")
93            fout.write("ceiling=" + str(mydata.ceiling) + "\n")
94            fout.write("floor=" + str(mydata.floor) + "\n")
95            fout.write("close=" + str(mydata.merge) + "\n")
96
97            # Walls
98            fout.write("#\n# Walls\n#\n")
99            fout.write("walls=" + str(mydata.wall_num) + "\n")
100            i = 0
101            for w in mydata.walls:
102                if i < mydata.wall_num:
103                    i += 1
104                    fout.write("w=" + str(round(w.w, 3)))
105                    # if w.a == True: # advance
106                    fout.write(",a=" + str(w.a) + ",")
107                    fout.write("r=" + str(round(w.r, 1)) + ",")
108                    fout.write("h=" + str(w.h) + ",")
109                    fout.write("m=" + str(round(w.m, 3)) + ",")
110                    fout.write("f=" + str(round(w.f, 3)) + ",")
111                    fout.write("c=" + str(w.curved) + ",")
112                    fout.write("cf=" + str(round(w.curve_factor, 1)) + ",")
113                    fout.write("cd=" + str(round(w.curve_arc_deg, 1)) + ",")
114                    fout.write("cs=" + str(w.curve_steps) + "\n")
115                    # else:
116                    # fOut.write("\n")
117
118            # Baseboard
119            fout.write("#\n# Baseboard\n#\n")
120            fout.write("baseboard=" + str(mydata.baseboard) + "\n")
121            fout.write("baseh=" + str(round(mydata.base_height, 3)) + "\n")
122            fout.write("baset=" + str(round(mydata.base_width, 3)) + "\n")
123            # Shell
124            fout.write("#\n# Wall Cover\n#\n")
125            fout.write("shell=" + str(mydata.shell) + "\n")
126            fout.write("shellh=" + str(round(mydata.shell_height, 3)) + "\n")
127            fout.write("shellt=" + str(round(mydata.shell_thick, 3)) + "\n")
128            fout.write("shellf=" + str(round(mydata.shell_factor, 3)) + "\n")
129            fout.write("shellb=" + str(round(mydata.shell_bfactor, 3)) + "\n")
130
131            # Materials
132            fout.write("#\n# Materials\n#\n")
133            fout.write("materials=" + str(mydata.crt_mat) + "\n")
134
135            fout.close()
136            self.report({'INFO'}, realpath + "successfully exported")
137        except:
138            self.report({'ERROR'}, "Unable to export room data")
139
140        return {'FINISHED'}
141
142    # ----------------------------------------------------------
143    # Invoke
144    # ----------------------------------------------------------
145
146    # noinspection PyUnusedLocal
147    def invoke(self, context, event):
148        context.window_manager.fileselect_add(self)
149        return {'RUNNING_MODAL'}
150
151
152# ----------------------------------------------------------
153# Import menu UI
154# ----------------------------------------------------------
155class ARCHIMESH_OT_ImportRoom(Operator, ImportHelper):
156    bl_idname = "io_import.roomdata"
157    bl_description = 'Import Room data (.dat)'
158    bl_category = 'View'
159    bl_label = "Import"
160
161    # From Helper. Filter filenames.
162    filename_ext = ".dat"
163    filter_glob: StringProperty(
164            default="*.dat",
165            options={'HIDDEN'},
166            )
167
168    filepath: StringProperty(
169            name="File Path",
170            description="File path used for exporting room data file",
171            maxlen=1024, default="",
172            )
173
174    # ----------------------------------------------------------
175    # Execute
176    # ----------------------------------------------------------
177    # noinspection PyUnusedLocal
178    def execute(self, context):
179        print("Importing:", self.properties.filepath)
180        # noinspection PyBroadException
181        try:
182            realpath = path.realpath(path.expanduser(self.properties.filepath))
183            finput = open(realpath)
184            line = finput.readline()
185
186            myobj = bpy.context.active_object
187            mydata = myobj.RoomGenerator[0]
188            # ----------------------------------
189            # Loop all records from file
190            # ----------------------------------
191            idx = 0  # index of each wall
192            while line:
193                if line[:1] != '#':
194                    if "name=" in line.lower():
195                        myobj.name = line[5:-1]
196
197                    elif "height=" in line.lower():
198                        mydata.room_height = float(line[7:-1])
199
200                    elif "thickness=" in line.lower():
201                        mydata.wall_width = float(line[10:-1])
202
203                    elif "inverse=" in line.lower():
204                        if line[8:-4].upper() == "T":
205                            mydata.inverse = True
206                        else:
207                            mydata.inverse = False
208
209                    elif "ceiling=" in line.lower():
210                        if line[8:-4].upper() == "T":
211                            mydata.ceiling = True
212                        else:
213                            mydata.ceiling = False
214
215                    elif "floor=" in line.lower():
216                        if line[6:-4].upper() == "T":
217                            mydata.floor = True
218                        else:
219                            mydata.floor = False
220
221                    elif "close=" in line.lower():
222                        if line[6:-4].upper() == "T":
223                            mydata.merge = True
224                        else:
225                            mydata.merge = False
226                    elif "baseboard=" in line.lower():
227                        if line[10:-4].upper() == "T":
228                            mydata.baseboard = True
229                        else:
230                            mydata.baseboard = False
231                    elif "baseh=" in line.lower():
232                        mydata.base_height = float(line[6:-1])
233                    elif "baset=" in line.lower():
234                        mydata.base_width = float(line[6:-1])
235                    elif "shell=" in line.lower():
236                        if line[6:-4].upper() == "T":
237                            mydata.shell = True
238                        else:
239                            mydata.shell = False
240                    elif "shellh=" in line.lower():
241                        mydata.shell_height = float(line[7:-1])
242                    elif "shellt=" in line.lower():
243                        mydata.shell_thick = float(line[6:-1])
244                    elif "shellf=" in line.lower():
245                        mydata.shell_factor = float(line[6:-1])
246                    elif "shellb=" in line.lower():
247                        mydata.shell_bfactor = float(line[6:-1])
248                    elif "walls=" in line.lower():
249                        mydata.wall_num = int(line[6:-1])
250
251                    # ---------------------
252                    # Walls Data
253                    # ---------------------
254                    elif "w=" in line.lower() and idx < mydata.wall_num:
255                        # get all pieces
256                        buf = line[:-1] + ","
257                        s = buf.split(",")
258                        for e in s:
259                            param = e.lower()
260                            if "w=" in param:
261                                mydata.walls[idx].w = float(e[2:])
262                            elif "a=" in param:
263                                if "true" == param[2:]:
264                                    mydata.walls[idx].a = True
265                                else:
266                                    mydata.walls[idx].a = False
267                            elif "r=" in param:
268                                mydata.walls[idx].r = float(e[2:])
269                            elif "h=" in param:
270                                mydata.walls[idx].h = e[2:]
271                            elif "m=" in param:
272                                mydata.walls[idx].m = float(e[2:])
273                            elif "f=" == param[0:2]:
274                                mydata.walls[idx].f = float(e[2:])
275                            elif "c=" in param:
276                                if "true" == param[2:]:
277                                    mydata.walls[idx].curved = True
278                                else:
279                                    mydata.walls[idx].curved = False
280                            elif "cf=" in param:
281                                mydata.walls[idx].curve_factor = float(e[3:])
282                            elif "cd=" in param:
283                                mydata.walls[idx].curve_arc_deg = float(e[3:])
284                            elif "cs=" in param:
285                                mydata.walls[idx].curve_steps = int(e[3:])
286                        idx += 1
287
288                    elif "materials=" in line.lower():
289                        if line[10:-4].upper() == "T":
290                            mydata.crt_mat = True
291                        else:
292                            mydata.crt_mat = False
293
294                line = finput.readline()
295
296            finput.close()
297            self.report({'INFO'}, realpath + "successfully imported")
298        except:
299            self.report({'ERROR'}, "Unable to import room data")
300
301        return {'FINISHED'}
302
303    # ----------------------------------------------------------
304    # Invoke
305    # ----------------------------------------------------------
306    # noinspection PyUnusedLocal
307    def invoke(self, context, event):
308        context.window_manager.fileselect_add(self)
309        return {'RUNNING_MODAL'}
310
311
312# ------------------------------------------------------------------
313# Define operator class to create rooms
314# ------------------------------------------------------------------
315class ARCHIMESH_OT_Room(Operator):
316    bl_idname = "mesh.archimesh_room"
317    bl_label = "Room"
318    bl_description = "Generate room with walls, baseboard, floor and ceiling"
319    bl_category = 'View'
320    bl_options = {'REGISTER', 'UNDO'}
321
322    # -----------------------------------------------------
323    # Draw (create UI interface)
324    # -----------------------------------------------------
325    # noinspection PyUnusedLocal
326    def draw(self, context):
327        layout = self.layout
328        row = layout.row()
329        row.label(text="Use Properties panel (N) to define parms", icon='INFO')
330        row = layout.row(align=False)
331        row.operator("io_import.roomdata", text="Import", icon='COPYDOWN')
332
333    # -----------------------------------------------------
334    # Execute
335    # -----------------------------------------------------
336    def execute(self, context):
337        if bpy.context.mode == "OBJECT":
338            create_room(self, context)
339            return {'FINISHED'}
340        else:
341            self.report({'WARNING'}, "Archimesh: Option only valid in Object mode")
342            return {'CANCELLED'}
343
344
345# ------------------------------------------------------------------------------
346# Create main object for the room. The other objects of room will be children of this.
347# ------------------------------------------------------------------------------
348# noinspection PyUnusedLocal
349def create_room(self, context):
350    # deselect all objects
351    for o in bpy.data.objects:
352        o.select_set(False)
353
354    # we create main object and mesh for walls
355    roommesh = bpy.data.meshes.new("Room")
356    roomobject = bpy.data.objects.new("Room", roommesh)
357    roomobject.location = bpy.context.scene.cursor.location
358    bpy.context.collection.objects.link(roomobject)
359    roomobject.RoomGenerator.add()
360    roomobject.RoomGenerator[0].walls.add()
361
362    # we shape the walls and create other objects as children of 'RoomObject'.
363    shape_walls_and_create_children(roomobject, roommesh)
364
365    # we select, and activate, main object for the room.
366    bpy.context.view_layer.objects.active = roomobject
367    roomobject.select_set(True)
368
369
370# -----------------------------------------------------
371# Verify if solidify exist
372# -----------------------------------------------------
373def is_solidify(myobject):
374    flag = False
375    try:
376        if myobject.modifiers is None:
377            return False
378
379        for mod in myobject.modifiers:
380            if mod.type == 'SOLIDIFY':
381                flag = True
382                break
383        return flag
384    except AttributeError:
385        return False
386
387
388# ------------------------------------------------------------------------------
389# Update wall mesh and children objects (baseboard, floor and ceiling).
390# ------------------------------------------------------------------------------
391# noinspection PyUnusedLocal
392def update_room(self, context):
393    # When we update, the active object is the main object of the room.
394    o = bpy.context.active_object
395    oldmesh = o.data
396    oldname = o.data.name
397    # Now we deselect that room object to not delete it.
398    o.select_set(False)
399    # and we create a new mesh for the walls:
400    tmp_mesh = bpy.data.meshes.new("temp")
401    # deselect all objects
402    for obj in bpy.data.objects:
403        obj.select_set(False)
404    # Remove children created by this addon:
405    for child in o.children:
406        # noinspection PyBroadException
407        try:
408            if child["archimesh.room_object"]:
409                # noinspection PyBroadException
410                try:
411                    # remove child relationship
412                    for grandchild in child.children:
413                        grandchild.parent = None
414                    # remove modifiers
415                    for mod in child.modifiers:
416                        bpy.ops.object.modifier_remove(mod)
417                except:
418                    pass
419                # clear data
420                old = child.data
421                child.select_set(True)
422                bpy.ops.object.delete()
423                bpy.data.meshes.remove(old)
424        except:
425            pass
426    # Finally we create all that again (except main object),
427    shape_walls_and_create_children(o, tmp_mesh, True)
428    o.data = tmp_mesh
429    # Remove data (mesh of active object),
430    bpy.data.meshes.remove(oldmesh)
431    tmp_mesh.name = oldname
432    # and select, and activate, the main object of the room.
433    o.select_set(True)
434    bpy.context.view_layer.objects.active = o
435
436
437# -----------------------------------------------------
438# Move Solidify to Top
439# -----------------------------------------------------
440def movetotopsolidify(myobject):
441    mymod = None
442    try:
443        if myobject.modifiers is not None:
444            for mod in myobject.modifiers:
445                if mod.type == 'SOLIDIFY':
446                    mymod = mod
447
448            if mymod is not None:
449                while myobject.modifiers[0] != mymod:
450                    bpy.ops.object.modifier_move_up(modifier=mymod.name)
451    except AttributeError:
452        return
453
454
455# ------------------------------------------------------------------------------
456# Generate walls, baseboard, floor, ceiling and materials.
457# For walls, it only shapes mesh and creates modifier solidify (the modifier, only the first time).
458# And, for the others, it creates object and mesh.
459# ------------------------------------------------------------------------------
460def shape_walls_and_create_children(myroom, tmp_mesh, update=False):
461    rp = myroom.RoomGenerator[0]  # "rp" means "room properties".
462    mybase = None
463    myfloor = None
464    myceiling = None
465    myshell = None
466    # Create the walls (only mesh, because the object is 'myRoom', created before).
467    create_walls(rp, tmp_mesh, get_blendunits(rp.room_height))
468    myroom.data = tmp_mesh
469    # Mark Seams
470    select_vertices(myroom, [0, 1])
471    mark_seam(myroom)
472    # Unwrap
473    unwrap_mesh(myroom)
474
475    remove_doubles(myroom)
476    set_normals(myroom, not rp.inverse)  # inside/outside
477
478    if rp.wall_width > 0.0:
479        if update is False or is_solidify(myroom) is False:
480            set_modifier_solidify(myroom, get_blendunits(rp.wall_width))
481        else:
482            for mod in myroom.modifiers:
483                if mod.type == 'SOLIDIFY':
484                    mod.thickness = rp.wall_width
485        # Move to Top SOLIDIFY
486        movetotopsolidify(myroom)
487
488    else:  # clear not used SOLIDIFY
489        for mod in myroom.modifiers:
490            if mod.type == 'SOLIDIFY':
491                myroom.modifiers.remove(mod)
492
493    # Create baseboard
494    if rp.baseboard:
495        baseboardmesh = bpy.data.meshes.new("Baseboard")
496        mybase = bpy.data.objects.new("Baseboard", baseboardmesh)
497        mybase.location = (0, 0, 0)
498        bpy.context.collection.objects.link(mybase)
499        mybase.parent = myroom
500        mybase.select_set(True)
501        mybase["archimesh.room_object"] = True
502        mybase["archimesh.room_baseboard"] = True
503
504        create_walls(rp, baseboardmesh, get_blendunits(rp.base_height), True)
505        set_normals(mybase, rp.inverse)  # inside/outside room
506        if rp.base_width:
507            set_modifier_solidify(mybase, get_blendunits(rp.base_width))
508            # Move to Top SOLIDIFY
509            movetotopsolidify(mybase)
510        # Mark Seams
511        select_vertices(mybase, [0, 1])
512        mark_seam(mybase)
513        # Unwrap
514        unwrap_mesh(mybase)
515
516    # Create floor
517    if rp.floor and rp.wall_num > 1:
518        myfloor = create_floor(rp, "Floor", myroom)
519        myfloor["archimesh.room_object"] = True
520        myfloor.parent = myroom
521        # Unwrap
522        unwrap_mesh(myfloor)
523
524    # Create ceiling
525    if rp.ceiling and rp.wall_num > 1:
526        myceiling = create_floor(rp, "Ceiling", myroom)
527        myceiling["archimesh.room_object"] = True
528        myceiling.parent = myroom
529        # Unwrap
530        unwrap_mesh(myceiling)
531
532    # Create Shell
533    #
534    if rp.shell:
535        myshell = add_shell(myroom, "Wall_cover", rp)
536        myshell["archimesh.room_object"] = True
537        myshell["archimesh.room_shell"] = True
538        parentobject(myroom, myshell)
539        myshell.rotation_euler = myroom.rotation_euler
540        if rp.wall_width > 0.0:
541            # Solidify (need for boolean)
542            set_modifier_solidify(myshell, 0.01)
543            # Move to Top SOLIDIFY
544            movetotopsolidify(mybase)
545
546    # Create materials
547    if rp.crt_mat and bpy.context.scene.render.engine in {'CYCLES', 'BLENDER_EEVEE'}:
548        # Wall material (two faces)
549        mat = create_diffuse_material("Wall_material", False, 0.765, 0.650, 0.588, 0.8, 0.621, 0.570, 0.1, True)
550        set_material(myroom, mat)
551
552        # Baseboard material
553        if rp.baseboard and mybase is not None:
554            mat = create_diffuse_material("Baseboard_material", False, 0.8, 0.8, 0.8)
555            set_material(mybase, mat)
556
557        # Ceiling material
558        if rp.ceiling and myceiling is not None:
559            mat = create_diffuse_material("Ceiling_material", False, 0.95, 0.95, 0.95)
560            set_material(myceiling, mat)
561
562        # Floor material
563        if rp.floor and myfloor is not None:
564            mat = create_brick_material("Floor_material", False, 0.711, 0.668, 0.668, 0.8, 0.636, 0.315)
565            set_material(myfloor, mat)
566
567        # Shell material
568        if rp.shell and myshell is not None:
569            mat = create_diffuse_material("Wall_cover_material", False, 0.507, 0.309, 0.076, 0.507, 0.309, 0.076)
570            set_material(myshell, mat)
571
572    # deactivate others
573    for o in bpy.data.objects:
574        if o.select_get() is True and o.name != myroom.name:
575            o.select_set(False)
576
577
578# ------------------------------------------------------------------------------
579# Create walls or baseboard (indicated with baseboard parameter).
580# Some custom values are passed using the rp ("room properties" group) parameter (rp.myvariable).
581# ------------------------------------------------------------------------------
582def create_walls(rp, mymesh, height, baseboard=False):
583    myvertex = [(0.0, 0.0, height), (0.0, 0.0, 0.0)]
584    myfaces = []
585    lastface = 0
586    lastx = lasty = 0
587    idf = 0
588    # Iterate the walls
589    for i in range(0, rp.wall_num):
590        if 0 == i:
591            prv = False
592        else:
593            prv = rp.walls[i - 1].a and not rp.walls[i - 1].curved
594
595        mydat = make_wall(prv, rp.walls[i], baseboard, lastface,
596                          lastx, lasty, height, myvertex, myfaces)
597        lastx = mydat[0]
598        lasty = mydat[1]
599        lastface = mydat[2]
600
601        # --------------------------------------
602        # saves vertex data for opengl
603        # --------------------------------------
604        point_a = None
605        point_b = None
606        try:
607            for mf in myfaces[idf]:
608                if myvertex[mf][2] == 0:
609                    if point_a is None:
610                        point_a = myvertex[mf]
611                    else:
612                        point_b = myvertex[mf]
613
614            rp.walls[i].glpoint_a = point_a
615            rp.walls[i].glpoint_b = point_b
616        except IndexError:
617            pass
618
619        idf = len(myfaces)
620
621    # Close room
622    if rp.merge is True:
623        if baseboard is False:
624            if rp.walls[rp.wall_num - 1].a is not True:
625                myfaces.extend([(0, 1, lastface + 1, lastface)])
626            else:
627                if rp.walls[rp.wall_num - 1].curved is True:
628                    myfaces.extend([(0, 1, lastface + 1, lastface)])
629                else:
630                    myfaces.extend([(0, 1, lastface, lastface + 1)])
631        else:
632            myfaces.extend([(0, 1, lastface + 1, lastface)])
633
634    mymesh.from_pydata(myvertex, [], myfaces)
635    mymesh.update(calc_edges=True)
636
637
638# ------------------------------------------------------------------------------
639# Make a Wall
640#   prv: If previous wall has 'curved' activate.
641#   lastFace: Number of faces of all before walls.
642#   lastX: X position of the end of the last wall.
643#   lastY: Y position of the end of the last wall.
644#   height: Height of the last wall, without peak.
645# ------------------------------------------------------------------------------
646def make_wall(prv, wall, baseboard, lastface, lastx, lasty, height, myvertex, myfaces):
647    #   size: Length of the wall.
648    #   over: Height of the peak from "height".
649    #   factor: Displacement of the peak (between -1 and 1; 0 is the middle of the wall).
650    advanced = wall.a
651    size = wall.w
652    over = wall.m
653    factor = wall.f
654    angle = wall.r
655    hide = wall.h
656
657    # if angle negative, calculate real
658    # use add because the angle is negative
659    if angle < 0:
660        angle += 360
661    # Verify Units
662    size = get_blendunits(size)
663    over = get_blendunits(over)
664
665    # Calculate size using angle
666    sizex = cos(radians(angle)) * size
667    sizey = sin(radians(angle)) * size
668
669    # Create faces
670    if advanced is False or baseboard is True:
671        # Cases of this first option: Baseboard or wall without peak and without curve.
672        if baseboard is True and advanced is True and wall.curved is True:
673            (myvertex, myfaces, sizex, sizey, lastface) = make_curved_wall(myvertex, myfaces, size, angle,
674                                                                           lastx, lasty, height, lastface,
675                                                                           wall.curve_factor, int(wall.curve_arc_deg),
676                                                                           int(wall.curve_arc_deg / wall.curve_steps),
677                                                                           hide, baseboard)
678        else:
679            myvertex.extend([(lastx + sizex, lasty + sizey, height),
680                             (lastx + sizex, lasty + sizey, 0.0)])
681            if check_visibility(hide, baseboard):
682                if prv is False or baseboard is True:
683                    # Previous no advance or advance with curve
684                    myfaces.extend([(lastface, lastface + 2, lastface + 3, lastface + 1)])
685                else:
686                    # Previous advance without curve
687                    myfaces.extend([(lastface, lastface + 1, lastface + 2, lastface + 3)])
688            lastface += 2
689    else:
690        # Case of this second option: Wall with advanced features (orientation, visibility and peak or curve).
691        # Orientation and visibility options ('angle' and 'hide' variables) are only visible in panel
692        # with advanced features, but are taken in account in any case.
693        if wall.curved:
694            # Wall with curve and without peak.
695            (myvertex, myfaces, sizex, sizey, lastface) = make_curved_wall(myvertex, myfaces, size, angle,
696                                                                           lastx, lasty, height, lastface,
697                                                                           wall.curve_factor, int(wall.curve_arc_deg),
698                                                                           int(wall.curve_arc_deg / wall.curve_steps),
699                                                                           hide, baseboard)
700        else:
701            # Wall with peak and without curve.
702            mid = size / 2 + ((size / 2) * factor)
703            midx = cos(radians(angle)) * mid
704            midy = sin(radians(angle)) * mid
705            # first face
706            myvertex.extend([(lastx + midx, lasty + midy, height + over),
707                             (lastx + midx, lasty + midy, 0.0)])
708            if check_visibility(hide, baseboard):
709                if fabs(factor) != 1:
710                    if prv is False:
711                        # Previous no advance or advance with curve
712                        myfaces.extend([(lastface, lastface + 2, lastface + 3, lastface + 1)])
713                    else:
714                        # Previous advance without curve
715                        myfaces.extend([(lastface, lastface + 1, lastface + 2, lastface + 3)])
716            # second face
717            myvertex.extend([(lastx + sizex, lasty + sizey, 0.0),
718                             (lastx + sizex, lasty + sizey, height)])
719            if check_visibility(hide, baseboard):
720                if fabs(factor) != 1:
721                    myfaces.extend([(lastface + 2, lastface + 5, lastface + 4, lastface + 3)])
722                else:
723                    if prv is False:
724                        myfaces.extend([(lastface, lastface + 5, lastface + 4, lastface + 1),
725                                        (lastface, lastface + 2, lastface + 5)])
726                    else:
727                        myfaces.extend([(lastface, lastface + 4, lastface + 5, lastface + 1),
728                                        (lastface + 1, lastface + 2, lastface + 5)])
729
730            lastface += 4
731
732    lastx += sizex
733    lasty += sizey
734
735    return lastx, lasty, lastface
736
737
738# ------------------------------------------------------------------------------
739# Verify visibility of walls
740# ------------------------------------------------------------------------------
741def check_visibility(h, base):
742    # Visible
743    if h == '0':
744        return True
745    # Wall
746    if h == '2':
747        if base is True:
748            return False
749        else:
750            return True
751    # Baseboard
752    if h == '1':
753        if base is True:
754            return True
755        else:
756            return False
757    # Hidden
758    if h == '3':
759        return False
760
761
762# ------------------------------------------------------------------------------
763# Create a curved wall.
764# ------------------------------------------------------------------------------
765def make_curved_wall(myvertex, myfaces, size, wall_angle, lastx, lasty, height,
766                     lastface, curve_factor, arc_angle, step_angle, hide, baseboard):
767    curvex = None
768    curvey = None
769    # Calculate size using angle
770    sizex = cos(radians(wall_angle)) * size
771    sizey = sin(radians(wall_angle)) * size
772
773    for step in range(0, arc_angle + step_angle, step_angle):
774        curvex = sizex / 2 - cos(radians(step + wall_angle)) * size / 2
775        curvey = sizey / 2 - sin(radians(step + wall_angle)) * size / 2
776        curvey = curvey * curve_factor
777        myvertex.extend([(lastx + curvex, lasty + curvey, height),
778                         (lastx + curvex, lasty + curvey, 0.0)])
779        if check_visibility(hide, baseboard):
780            myfaces.extend([(lastface, lastface + 2, lastface + 3, lastface + 1)])
781        lastface += 2
782    return myvertex, myfaces, curvex, curvey, lastface
783
784
785# ------------------------------------------------------------------------------
786# Create floor or ceiling (create object and mesh)
787# Parameters:
788#   rm: "room properties" group
789#   typ: Name of new object and mesh ('Floor' or 'Ceiling')
790#   myRoom: Main object for the room
791# ------------------------------------------------------------------------------
792
793def create_floor(rp, typ, myroom):
794    bpy.context.view_layer.objects.active = myroom
795
796    myvertex = []
797    myfaces = []
798    verts = []
799
800    obverts = bpy.context.active_object.data.vertices
801    for vertex in obverts:
802        verts.append(tuple(vertex.co))
803    # Loop only selected
804    i = 0
805    for e in verts:
806        if typ == "Floor":
807            if e[2] == 0.0:
808                myvertex.extend([(e[0], e[1], e[2])])
809                i += 1
810        else:  # ceiling
811            if round(e[2], 5) == round(get_blendunits(rp.room_height), 5):
812                myvertex.extend([(e[0], e[1], e[2])])
813                i += 1
814
815    # Create faces
816    fa = []
817    for f in range(0, i):
818        fa.extend([f])
819
820    myfaces.extend([fa])
821
822    mymesh = bpy.data.meshes.new(typ)
823    myobject = bpy.data.objects.new(typ, mymesh)
824
825    myobject.location = (0, 0, 0)
826    bpy.context.collection.objects.link(myobject)
827
828    mymesh.from_pydata(myvertex, [], myfaces)
829    mymesh.update(calc_edges=True)
830
831    return myobject
832
833
834# ------------------------------------------------------------------
835# Define property group class to create, or modify, room walls.
836# ------------------------------------------------------------------
837class WallProperties(PropertyGroup):
838    w: FloatProperty(
839            name='Length',
840            min=-150, max=150,
841            default=1, precision=3,
842            description='Length of the wall (negative to reverse direction)',
843            update=update_room,
844            )
845
846    a: BoolProperty(
847            name="Advanced",
848            description="Define advanced parameters of the wall",
849            default=False,
850            update=update_room,
851            )
852
853    curved: BoolProperty(
854            name="Curved",
855            description="Enable curved wall parameters",
856            default=False,
857            update=update_room,
858            )
859    curve_factor: FloatProperty(
860            name='Factor',
861            min=-5, max=5,
862            default=1, precision=1,
863            description='Curvature variation',
864            update=update_room,
865            )
866    curve_arc_deg: FloatProperty(
867            name='Degrees', min=1, max=359,
868            default=180, precision=1,
869            description='Degrees of the curve arc (must be >= steps)',
870            update=update_room,
871            )
872    curve_steps: IntProperty(
873            name='Steps',
874            min=2, max=50,
875            default=12,
876            description='Curve steps',
877            update=update_room,
878            )
879
880    m: FloatProperty(
881            name='Peak', min=0, max=50,
882            default=0, precision=3,
883            description='Middle height variation',
884            update=update_room,
885            )
886    f: FloatProperty(
887            name='Factor', min=-1, max=1,
888            default=0, precision=3,
889            description='Middle displacement',
890            update=update_room,
891            )
892    r: FloatProperty(
893            name='Angle',
894            min=-180, max=180,
895            default=0, precision=1,
896            description='Wall Angle (-180 to +180)',
897            update=update_room,
898            )
899
900    h: EnumProperty(
901            items=(
902                ('0', "Visible", ""),
903                ('1', "Baseboard", ""),
904                ('2', "Wall", ""),
905                ('3', "Hidden", ""),
906                ),
907            name="",
908            description="Wall visibility",
909            update=update_room,
910            )
911
912    # opengl internal data
913    glpoint_a: FloatVectorProperty(
914            name="glpointa",
915            description="Hidden property for opengl",
916            default=(0, 0, 0),
917            )
918    glpoint_b: FloatVectorProperty(
919            name="glpointb",
920            description="Hidden property for opengl",
921            default=(0, 0, 0),
922            )
923
924bpy.utils.register_class(WallProperties)
925
926
927# ------------------------------------------------------------------
928# Add a new room wall.
929# First add a parameter group for that new wall, and then update the room.
930# ------------------------------------------------------------------
931def add_room_wall(self, context):
932    rp = context.object.RoomGenerator[0]
933    for cont in range(len(rp.walls) - 1, rp.wall_num):
934        rp.walls.add()
935        # by default, we alternate the direction of the walls.
936        if 1 == cont % 2:
937            rp.walls[cont].r = 90
938    update_room(self, context)
939
940
941# ------------------------------------
942# Get if some vertex is highest
943# ------------------------------------
944def get_hight(verts, faces_4, faces_3, face_index, face_num):
945    rtn = face_index
946    a = faces_4[face_num][0]
947    b = faces_4[face_num][1]
948    c = faces_4[face_num][2]
949    d = faces_4[face_num][3]
950
951    for face3 in faces_3:
952        for idx3 in face3:
953            if idx3 != face_index:
954                # check x and y position (must be equal)
955                if verts[idx3][0] == verts[face_index][0] and verts[idx3][1] == verts[face_index][1]:
956                    # only if z is > that previous z
957                    if verts[idx3][2] > verts[face_index][2]:
958                        # checking if the original vertex is in the same face
959                        # must have 2 vertices on the original face
960                        t = 0
961                        for e in face3:
962                            if e == a or e == b or e == c or e == d:
963                                t += 1
964                        if t >= 2:
965                            rtn = idx3
966
967    return rtn
968
969
970# ------------------------------------
971# Sort list of faces
972# ------------------------------------
973def sort_facelist(activefaces, activenormals):
974    totfaces = len(activefaces)
975    newlist = []
976    newnormal = []
977    # -----------------------
978    # Only one face
979    # -----------------------
980    if totfaces == 1:
981        newlist.append(activefaces[0])
982        newnormal.append(activenormals[0])
983        return newlist, newnormal
984
985    # -----------------------
986    # Look for first element
987    # -----------------------
988    idx = 0
989    for face in activefaces:
990        c = 0
991        for i in face:
992            if i == 0 or i == 1:
993                c += 1
994
995        if c >= 2 and face not in newlist:
996            newlist.append(face)
997            newnormal.append(activenormals[idx])
998            break
999        idx += 1
1000
1001    # -----------------------
1002    # Look for second element
1003    # -----------------------
1004    idx = 0
1005    for face in activefaces:
1006        c = 0
1007        for i in face:
1008            if i == 2 or i == 3:
1009                c += 1
1010        if c >= 2 and face not in newlist:
1011            newlist.append(face)
1012            newnormal.append(activenormals[idx])
1013            break
1014        idx += 1
1015
1016    # -----------------------
1017    # Add next faces
1018    # -----------------------
1019    for x in range(2, totfaces):
1020        idx = 0
1021        for face in activefaces:
1022            c = 0
1023            for i in face:
1024                if i == newlist[x - 1][0] or i == newlist[x - 1][1] or i == newlist[x - 1][2] or i == newlist[x - 1][3]:
1025                    c += 1
1026            if c >= 2 and face not in newlist:
1027                newlist.append(face)
1028                newnormal.append(activenormals[idx])
1029            idx += 1
1030
1031    return newlist, newnormal
1032
1033
1034# ------------------------------------
1035# Get points of the walls
1036# selobject: room
1037# ------------------------------------
1038def get_wall_points(selobject):
1039    obverts = selobject.data.vertices
1040    obfaces = selobject.data.polygons
1041
1042    verts = []
1043    faces_3 = []
1044    faces_4 = []
1045    normals = []
1046    activefaces = []
1047    activenormals = []
1048
1049    # --------------------------
1050    # Recover all vertex
1051    # --------------------------
1052    for vertex in obverts:
1053        verts.append(list(vertex.co))
1054
1055    # --------------------------
1056    # Recover 3 faces
1057    # --------------------------
1058    for face in obfaces:
1059        # get only 4 corners faces
1060        if len(list(face.vertices)) == 3:
1061            faces_3.append(list(face.vertices))
1062    # --------------------------
1063    # Recover 4 faces
1064    # --------------------------
1065    for face in obfaces:
1066        # get only 4 corners faces
1067        if len(list(face.vertices)) == 4:
1068            faces_4.append(list(face.vertices))
1069            normals.append(face.normal)
1070    # --------------------------
1071    # Replace highest
1072    # --------------------------
1073    idx = 0
1074    for face in faces_4:
1075        mylist = []
1076        for e in face:  # e contains the number of vertex element
1077            if verts[e][2] == 0:
1078                mylist.append(e)
1079            # Only if Z > 0, recalculate
1080            if verts[e][2] != 0:
1081                mylist.append(get_hight(verts, faces_4, faces_3, e, idx))
1082
1083        activefaces.append(mylist)
1084        activenormals.append(normals[idx])
1085        idx += 1
1086
1087    # ------------------------
1088    # Sort faces
1089    # ------------------------
1090    newlist, newnormal = sort_facelist(activefaces, activenormals)
1091
1092    return verts, newlist, newnormal
1093
1094
1095# ------------------------------------
1096# Create a shell of boards
1097# selobject: room
1098# objname: Name for new object
1099# rp: room properties
1100# ------------------------------------
1101def add_shell(selobject, objname, rp):
1102
1103    myvertex = []
1104    myfaces = []
1105
1106    verts, activefaces, activenormals = get_wall_points(selobject)
1107
1108    # --------------------------
1109    # Get line points
1110    # --------------------------
1111    i = 0
1112    idx = 0
1113    for face in activefaces:
1114        a1 = None
1115        b1 = None
1116        a2 = None
1117        b2 = None
1118        # Bottom
1119        for e in face:
1120            if verts[e][2] == 0:
1121                if a1 is None:
1122                    a1 = e
1123                else:
1124                    b1 = e
1125        # Top
1126        for e in face:
1127            if verts[e][2] != 0:
1128                if verts[a1][0] == verts[e][0] and verts[a1][1] == verts[e][1]:
1129                    a2 = e
1130                else:
1131                    b2 = e
1132        # Create the mesh
1133        mydata = create_cover_mesh(idx, verts, activefaces, activenormals, i, a1, a2, b1, b2,
1134                                   rp.merge, 0.005,
1135                                   rp.shell_height, rp.shell_thick, rp.shell_factor, rp.shell_bfactor)
1136        i = mydata[0]
1137        myvertex.extend(mydata[1])
1138        myfaces.extend(mydata[2])
1139        idx += 1
1140    # --------------------------
1141    # Create the mesh
1142    # --------------------------
1143    mesh = bpy.data.meshes.new(objname)
1144    myobject = bpy.data.objects.new(objname, mesh)
1145
1146    myobject.location = selobject.location
1147    bpy.context.collection.objects.link(myobject)
1148
1149    mesh.from_pydata(myvertex, [], myfaces)
1150    mesh.update(calc_edges=True)
1151
1152    remove_doubles(myobject)
1153    set_normals(myobject)
1154
1155    return myobject
1156
1157
1158# ---------------------------------------------------------
1159# Project point using face normals
1160#
1161# m: Magnitud
1162# pf: Comparison face +/-
1163# ---------------------------------------------------------
1164def project_point(idx, point, normals, m, pf):
1165    v1 = Vector(normals[idx])
1166    if idx + pf >= len(normals):
1167        vf = v1
1168    elif idx + pf < 0:
1169        vf = v1
1170    else:
1171        v2 = Vector(normals[idx + pf])
1172        if v1 != v2:
1173            vf = v1 + v2
1174            vf.normalize()  # must be length equal to 1
1175        else:
1176            vf = v1
1177
1178    n1 = (vf[0] * m, vf[1] * m, vf[2] * m)
1179    p1 = (point[0] + n1[0], point[1] + n1[1], point[2] + n1[2])
1180    return p1
1181
1182
1183# ---------------------------------------------------------
1184# Create wall cover mesh
1185#
1186# Uses linear equation for cutting
1187#
1188# Z = This value is the z axis value
1189# so, we can replace t with ((Z-Z1) / (Z2-Z1))
1190#
1191# X = X1 + ((X2 - X1) * t)
1192#
1193# X = X1 + ((X2 - X1) * ((Z-Z1) / (Z2-Z1)))
1194# Y = Y1 + ((Y2 - Y1) * ((Z-Z1) / (Z2-Z1)))
1195#
1196# height refers to the height of the cover piece
1197# width refers to the width of the cover piece
1198# ---------------------------------------------------------
1199
1200
1201def create_cover_mesh(idx, verts, activefaces, normals, i, a1, a2, b1, b2, merge, space=0.005,
1202                      height=0.20, thickness=0.025, shell_factor=1, shell_bfactor=1):
1203    pvertex = []
1204    pfaces = []
1205
1206    a1_x = verts[a1][0]
1207    a1_y = verts[a1][1]
1208    a1_z = verts[a1][2]
1209
1210    a2_x = verts[a2][0]
1211    a2_y = verts[a2][1]
1212    a2_z = verts[a2][2]
1213
1214    b1_x = verts[b1][0]
1215    b1_y = verts[b1][1]
1216    b1_z = verts[b1][2]
1217
1218    b2_x = verts[b2][0]
1219    b2_y = verts[b2][1]
1220    b2_z = verts[b2][2]
1221
1222    # Get highest
1223    if a2_z >= b2_z:
1224        top = a2_z
1225        limit = b2_z
1226    else:
1227        top = b2_z
1228        limit = a2_z
1229
1230    # apply factor
1231    # get high point of walls
1232    maxh = 0
1233    for v in verts:
1234        if v[2] > maxh:
1235            maxh = v[2]
1236    maxh *= shell_factor
1237    minh = maxh * (1 - shell_bfactor)
1238    if minh < 0:
1239        minh = 0
1240
1241    if shell_factor < 1:
1242        if top > maxh:
1243            top = maxh
1244
1245    # --------------------------------------
1246    # Loop to generate each piece of cover
1247    # --------------------------------------
1248    zpos = minh  # initial position
1249    f = 0
1250    f2 = 0
1251    # detect what face must use to compare
1252    face_num = len(activefaces) - 1
1253    if idx == 0 and merge is True:
1254        if is_in_nextface(idx + 1, activefaces, verts, a1_x, a1_y) is True:
1255            side_a = 1
1256            side_b = face_num
1257        else:
1258            side_a = face_num
1259            side_b = 1
1260    elif idx == face_num and merge is True:
1261        if is_in_nextface(face_num, activefaces, verts, a1_x, a1_y) is False:
1262            side_b = -face_num
1263            side_a = -1
1264        else:
1265            side_b = -1
1266            side_a = -face_num
1267    else:
1268        if is_in_nextface(idx + 1, activefaces, verts, a1_x, a1_y) is True:
1269            side_a = 1
1270            side_b = -1
1271        else:
1272            side_a = -1
1273            side_b = 1
1274            # Last wall
1275            if idx + 1 >= len(activefaces):
1276                if is_in_nextface(idx - 1, activefaces, verts, a1_x, a1_y) is True:
1277                    side_a = -1
1278                    side_b = 1
1279                else:
1280                    side_a = 1
1281                    side_b = -1
1282
1283    na1_x = 0
1284    na1_y = 0
1285    na2_x = 0
1286    na2_y = 0
1287
1288    nb1_x = 0
1289    nb1_y = 0
1290    nb2_x = 0
1291    nb2_y = 0
1292
1293    nc1_x = 0
1294    nc1_y = 0
1295    nc2_x = 0
1296    nc2_y = 0
1297
1298    nd1_x = 0
1299    nd1_y = 0
1300    nd2_x = 0
1301    nd2_y = 0
1302
1303    while zpos <= top:
1304        # ----------------------
1305        # Full cover piece
1306        # ----------------------
1307        if zpos <= limit:
1308            # ----------------
1309            # Point A
1310            # ----------------
1311            mypoint = project_point(idx, (a1_x, a1_y, zpos), normals, space, side_a)
1312
1313            pvertex.extend([mypoint])
1314            na1_x = mypoint[0]
1315            na1_y = mypoint[1]
1316            # external point
1317            mypoint = project_point(idx, (a1_x, a1_y, zpos), normals, space + thickness, side_a)
1318            pvertex.extend([mypoint])
1319            nc1_x = mypoint[0]
1320            nc1_y = mypoint[1]
1321            # get second point (vertical)
1322            mypoint = project_point(idx, (a2_x, a2_y, zpos), normals, space, side_a)
1323            na2_x = mypoint[0]
1324            na2_y = mypoint[1]
1325            mypoint = project_point(idx, (a2_x, a2_y, zpos), normals, space + thickness, side_a)
1326            nc2_x = mypoint[0]
1327            nc2_y = mypoint[1]
1328
1329            # ----------------
1330            # Point B
1331            # ----------------
1332            mypoint = project_point(idx, (b1_x, b1_y, zpos), normals, space, side_b)
1333            pvertex.extend([mypoint])
1334            nb1_x = mypoint[0]
1335            nb1_y = mypoint[1]
1336            # external point
1337            mypoint = project_point(idx, (b1_x, b1_y, zpos), normals, space + thickness, side_b)
1338            pvertex.extend([mypoint])
1339            nd1_x = mypoint[0]
1340            nd1_y = mypoint[1]
1341            # get second point (vertical)
1342            mypoint = project_point(idx, (b2_x, b2_y, zpos), normals, space, side_b)
1343            nb2_x = mypoint[0]
1344            nb2_y = mypoint[1]
1345            mypoint = project_point(idx, (b2_x, b2_y, zpos), normals, space + thickness, side_b)
1346            nd2_x = mypoint[0]
1347            nd2_y = mypoint[1]
1348
1349            # Faces
1350            if zpos != top:
1351                pfaces.extend([(i, i + 1, i + 3, i + 2)])
1352
1353            if f >= 1:
1354                pfaces.extend([(i - 3, i, i + 2, i - 1)])
1355
1356            i += 4
1357            f += 1
1358        # ----------------------
1359        # Cut pieces
1360        # ----------------------
1361        else:
1362            # -------------------------------
1363            # Internal Points
1364            # -------------------------------
1365            # Get highest
1366            if a2_z >= b2_z:
1367                ax1 = na1_x
1368                ay1 = na1_y
1369                az1 = a1_z
1370                ax2 = na2_x
1371                ay2 = na2_y
1372                az2 = a2_z
1373
1374                bx1 = na2_x
1375                by1 = na2_y
1376                bz1 = a2_z
1377                bx2 = nb2_x
1378                by2 = nb2_y
1379                bz2 = b2_z
1380            else:
1381                ax1 = na2_x
1382                ay1 = na2_y
1383                az1 = a2_z
1384                ax2 = nb2_x
1385                ay2 = nb2_y
1386                az2 = b2_z
1387
1388                bx1 = nb1_x
1389                by1 = nb1_y
1390                bz1 = b1_z
1391                bx2 = nb2_x
1392                by2 = nb2_y
1393                bz2 = b2_z
1394
1395            # ----------------
1396            # Point A
1397            # ----------------
1398            x = ax1 + ((ax2 - ax1) * ((zpos - az1) / (az2 - az1)))
1399            y = ay1 + ((ay2 - ay1) * ((zpos - az1) / (az2 - az1)))
1400            pvertex.extend([(x, y, zpos)])
1401            # ----------------
1402            # Point B
1403            # ----------------
1404            x = bx1 + ((bx2 - bx1) * ((zpos - bz1) / (bz2 - bz1)))
1405            y = by1 + ((by2 - by1) * ((zpos - bz1) / (bz2 - bz1)))
1406            pvertex.extend([(x, y, zpos)])
1407            # -------------------------------
1408            # External Points
1409            # -------------------------------
1410            # Get highest
1411            if a2_z >= b2_z:
1412                ax1 = nc1_x
1413                ay1 = nc1_y
1414                az1 = a1_z
1415                ax2 = nc2_x
1416                ay2 = nc2_y
1417                az2 = a2_z
1418
1419                bx1 = nc2_x
1420                by1 = nc2_y
1421                bz1 = a2_z
1422                bx2 = nd2_x
1423                by2 = nd2_y
1424                bz2 = b2_z
1425            else:
1426                ax1 = nc2_x
1427                ay1 = nc2_y
1428                az1 = a2_z
1429                ax2 = nd2_x
1430                ay2 = nd2_y
1431                az2 = b2_z
1432
1433                bx1 = nd1_x
1434                by1 = nd1_y
1435                bz1 = b1_z
1436                bx2 = nd2_x
1437                by2 = nd2_y
1438                bz2 = b2_z
1439
1440            # ----------------
1441            # Point A
1442            # ----------------
1443            x = ax1 + ((ax2 - ax1) * ((zpos - az1) / (az2 - az1)))
1444            y = ay1 + ((ay2 - ay1) * ((zpos - az1) / (az2 - az1)))
1445            pvertex.extend([(x, y, zpos)])
1446            # ----------------
1447            # Point B
1448            # ----------------
1449            x = bx1 + ((bx2 - bx1) * ((zpos - bz1) / (bz2 - bz1)))
1450            y = by1 + ((by2 - by1) * ((zpos - bz1) / (bz2 - bz1)))
1451            pvertex.extend([(x, y, zpos)])
1452            # Faces
1453            if zpos != top:
1454                pfaces.extend([(i, i + 1, i + 3, i + 2)])
1455
1456            if f2 == 0:
1457                pfaces.extend([(i - 1, i - 3, i, i + 1)])
1458            else:
1459                pfaces.extend([(i - 1, i - 2, i, i + 1)])
1460
1461            i += 4
1462            f2 += 1
1463        # avoid infinite loop
1464        if zpos == top:
1465            break
1466
1467        # add new piece
1468        zpos += height
1469        # cut oversized
1470        if zpos > top:
1471            zpos = top
1472
1473    return i, pvertex, pfaces
1474
1475
1476# -------------------------------------------------------------
1477# Detect if the vertex is face
1478# -------------------------------------------------------------
1479def is_in_nextface(idx, activefaces, verts, x, y):
1480    if idx >= len(activefaces):
1481        return False
1482
1483    for f in activefaces[idx]:
1484        if verts[f][2] == 0:  # only ground
1485            if verts[f][0] == x and verts[f][1] == y:
1486                return True
1487
1488    return False
1489
1490
1491# ------------------------------------------------------------------
1492# Define property group class to create or modify a rooms.
1493# ------------------------------------------------------------------
1494class RoomProperties(PropertyGroup):
1495    room_height: FloatProperty(
1496            name='Height', min=0.001, max=50,
1497            default=2.4, precision=3,
1498            description='Room height', update=update_room,
1499            )
1500    wall_width: FloatProperty(
1501            name='Thickness', min=0.000, max=10,
1502            default=0.0, precision=3,
1503            description='Thickness of the walls', update=update_room,
1504            )
1505    inverse: BoolProperty(
1506            name="Inverse", description="Inverse normals to outside",
1507            default=False,
1508            update=update_room,
1509            )
1510    crt_mat: BoolProperty(
1511            name="Create default Cycles materials",
1512            description="Create default materials for Cycles render",
1513            default=True,
1514            update=update_room,
1515            )
1516
1517    wall_num: IntProperty(
1518            name='Number of Walls', min=1, max=50,
1519            default=1,
1520            description='Number total of walls in the room', update=add_room_wall,
1521            )
1522
1523    baseboard: BoolProperty(
1524            name="Baseboard", description="Create a baseboard automatically",
1525            default=True,
1526            update=update_room,
1527            )
1528
1529    base_width: FloatProperty(
1530            name='Width', min=-10, max=10,
1531            default=0.015, precision=3,
1532            description='Baseboard width', update=update_room,
1533            )
1534    base_height: FloatProperty(
1535            name='Height', min=0.05, max=20,
1536            default=0.12, precision=3,
1537            description='Baseboard height', update=update_room,
1538            )
1539
1540    ceiling: BoolProperty(
1541            name="Ceiling", description="Create a ceiling",
1542            default=False, update=update_room,
1543            )
1544    floor: BoolProperty(
1545            name="Floor", description="Create a floor automatically",
1546            default=False,
1547            update=update_room,
1548            )
1549
1550    merge: BoolProperty(
1551            name="Close walls", description="Close walls to create a full closed room",
1552            default=False, update=update_room,
1553            )
1554
1555    walls: CollectionProperty(
1556            type=WallProperties,
1557            )
1558
1559    shell: BoolProperty(
1560            name="Wall cover", description="Create a cover of boards",
1561            default=False, update=update_room,
1562            )
1563    shell_thick: FloatProperty(
1564            name='Thickness', min=0.001, max=1,
1565            default=0.025, precision=3,
1566            description='Cover board thickness', update=update_room,
1567            )
1568    shell_height: FloatProperty(
1569            name='Height', min=0.05, max=1,
1570            default=0.20, precision=3,
1571            description='Cover board height', update=update_room,
1572            )
1573    shell_factor: FloatProperty(
1574            name='Top', min=0.1, max=1,
1575            default=1, precision=1,
1576            description='Percentage for top covering (1 Full)', update=update_room,
1577            )
1578    shell_bfactor: FloatProperty(
1579            name='Bottom', min=0.1, max=1,
1580            default=1, precision=1,
1581            description='Percentage for bottom covering (1 Full)', update=update_room,
1582            )
1583
1584bpy.utils.register_class(RoomProperties)
1585Object.RoomGenerator = CollectionProperty(type=RoomProperties)
1586
1587
1588# -----------------------------------------------------
1589# Add wall parameters to the panel.
1590# -----------------------------------------------------
1591def add_wall(idx, box, wall):
1592    box.label(text="Wall " + str(idx))
1593    row = box.row()
1594    row.prop(wall, 'w')
1595    row.prop(wall, 'a')
1596    # row.prop(wall, 'curved')
1597    if wall.a is True:
1598        srow = box.row()
1599        srow.prop(wall, 'r')
1600        srow.prop(wall, 'h')
1601
1602        srow = box.row()
1603        srow.prop(wall, 'curved')
1604
1605        if wall.curved is False:
1606            srow.prop(wall, 'm')
1607            srow.prop(wall, 'f')
1608
1609        if wall.curved is True:
1610            srow.prop(wall, 'curve_factor')
1611            srow.prop(wall, 'curve_arc_deg')
1612            srow.prop(wall, 'curve_steps')
1613
1614
1615# ------------------------------------------------------------------
1616# Define panel class to modify rooms.
1617# ------------------------------------------------------------------
1618class ARCHIMESH_PT_RoomGenerator(Panel):
1619    bl_idname = "OBJECT_PT_room_generator"
1620    bl_label = "Room"
1621    bl_space_type = 'VIEW_3D'
1622    bl_region_type = 'UI'
1623    bl_category = 'Create'
1624
1625    # -----------------------------------------------------
1626    # Verify if visible
1627    # -----------------------------------------------------
1628    @classmethod
1629    def poll(cls, context):
1630        o = context.object
1631        if o is None:
1632            return False
1633        if 'RoomGenerator' not in o:
1634            return False
1635        else:
1636            return True
1637
1638    # -----------------------------------------------------
1639    # Draw (create UI interface)
1640    # -----------------------------------------------------
1641    def draw(self, context):
1642        o = context.object
1643        # If the selected object didn't be created with the group 'RoomGenerator', this panel is not created.
1644        # noinspection PyBroadException
1645        try:
1646            if 'RoomGenerator' not in o:
1647                return
1648        except:
1649            return
1650
1651        layout = self.layout
1652        if bpy.context.mode == 'EDIT_MESH':
1653            layout.label(text='Warning: Operator does not work in edit mode.', icon='ERROR')
1654        else:
1655            room = o.RoomGenerator[0]
1656            row = layout.row()
1657            row.prop(room, 'room_height')
1658            row.prop(room, 'wall_width')
1659            row.prop(room, 'inverse')
1660
1661            row = layout.row()
1662            if room.wall_num > 1:
1663                row.prop(room, 'ceiling')
1664                row.prop(room, 'floor')
1665                row.prop(room, 'merge')
1666
1667            # Wall number
1668            row = layout.row()
1669            row.prop(room, 'wall_num')
1670
1671            # Add menu for walls
1672            if room.wall_num > 0:
1673                for wall_index in range(0, room.wall_num):
1674                    box = layout.box()
1675                    add_wall(wall_index + 1, box, room.walls[wall_index])
1676
1677            box = layout.box()
1678            box.prop(room, 'baseboard')
1679            if room.baseboard is True:
1680                row = box.row()
1681                row.prop(room, 'base_width')
1682                row.prop(room, 'base_height')
1683
1684            box = layout.box()
1685            box.prop(room, 'shell')
1686            if room.shell is True:
1687                row = box.row()
1688                row.prop(room, 'shell_height')
1689                row.prop(room, 'shell_thick')
1690                row = box.row()
1691                row.prop(room, 'shell_factor', slider=True)
1692                row.prop(room, 'shell_bfactor', slider=True)
1693
1694            box = layout.box()
1695            if not context.scene.render.engine in {'CYCLES', 'BLENDER_EEVEE'}:
1696                box.enabled = False
1697            box.prop(room, 'crt_mat')
1698