diff options
Diffstat (limited to 'archimesh/achm_room_maker.py')
-rw-r--r-- | archimesh/achm_room_maker.py | 1700 |
1 files changed, 1700 insertions, 0 deletions
diff --git a/archimesh/achm_room_maker.py b/archimesh/achm_room_maker.py new file mode 100644 index 00000000..f7b03131 --- /dev/null +++ b/archimesh/achm_room_maker.py @@ -0,0 +1,1700 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +# ---------------------------------------------------------- +# Automatic generation of rooms +# Author: Antonio Vazquez (antonioya) and Eduardo Gutierrez +# +# ---------------------------------------------------------- +# noinspection PyUnresolvedReferences +import bpy +import math +# noinspection PyUnresolvedReferences +import mathutils +import datetime +import time +from achm_tools import * +# noinspection PyUnresolvedReferences +from bpy_extras.io_utils import ExportHelper, ImportHelper + + +# ---------------------------------------------------------- +# Export menu UI +# ---------------------------------------------------------- +class AchmExportRoom(bpy.types.Operator, ExportHelper): + bl_idname = "io_export.roomdata" + bl_description = 'Export Room data (.dat)' + bl_category = 'Archimesh' + bl_label = "Export" + + # From ExportHelper. Filter filenames. + filename_ext = ".dat" + filter_glob = bpy.props.StringProperty( + default="*.dat", + options={'HIDDEN'}, + ) + + filepath = bpy.props.StringProperty( + name="File Path", + description="File path used for exporting room data file", + maxlen=1024, default="", + ) + + # ---------------------------------------------------------- + # Execute + # ---------------------------------------------------------- + # noinspection PyUnusedLocal + def execute(self, context): + print("Exporting:", self.properties.filepath) + # noinspection PyBroadException + try: + myobj = bpy.context.active_object + mydata = myobj.RoomGenerator[0] + + # ------------------------------- + # extract path and filename + # ------------------------------- + (filepath, filename) = os.path.split(self.properties.filepath) + print('Exporting %s' % filename) + # ------------------------------- + # Open output file + # ------------------------------- + realpath = os.path.realpath(os.path.expanduser(self.properties.filepath)) + fout = open(realpath, 'w') + + st = datetime.datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d %H:%M:%S') + fout.write("# Archimesh room export data\n") + fout.write("# " + st + "\n") + fout.write("#======================================================\n") + + fout.write("name=" + myobj.name + "\n") + fout.write("height=" + str(round(mydata.room_height, 3)) + "\n") + fout.write("thickness=" + str(round(mydata.wall_width, 3)) + "\n") + fout.write("inverse=" + str(mydata.inverse) + "\n") + fout.write("ceiling=" + str(mydata.ceiling) + "\n") + fout.write("floor=" + str(mydata.floor) + "\n") + fout.write("close=" + str(mydata.merge) + "\n") + + # Walls + fout.write("#\n# Walls\n#\n") + fout.write("walls=" + str(mydata.wall_num) + "\n") + i = 0 + for w in mydata.walls: + if i < mydata.wall_num: + i += 1 + fout.write("w=" + str(round(w.w, 3))) + # if w.a == True: # advance + fout.write(",a=" + str(w.a) + ",") + fout.write("r=" + str(round(w.r, 1)) + ",") + fout.write("h=" + str(w.h) + ",") + fout.write("m=" + str(round(w.m, 3)) + ",") + fout.write("f=" + str(round(w.f, 3)) + ",") + fout.write("c=" + str(w.curved) + ",") + fout.write("cf=" + str(round(w.curve_factor, 1)) + ",") + fout.write("cd=" + str(round(w.curve_arc_deg, 1)) + ",") + fout.write("cs=" + str(w.curve_steps) + "\n") + # else: + # fOut.write("\n") + + # Baseboard + fout.write("#\n# Baseboard\n#\n") + fout.write("baseboard=" + str(mydata.baseboard) + "\n") + fout.write("baseh=" + str(round(mydata.base_height, 3)) + "\n") + fout.write("baset=" + str(round(mydata.base_width, 3)) + "\n") + # Shell + fout.write("#\n# Wall Cover\n#\n") + fout.write("shell=" + str(mydata.shell) + "\n") + fout.write("shellh=" + str(round(mydata.shell_height, 3)) + "\n") + fout.write("shellt=" + str(round(mydata.shell_thick, 3)) + "\n") + fout.write("shellf=" + str(round(mydata.shell_factor, 3)) + "\n") + fout.write("shellb=" + str(round(mydata.shell_bfactor, 3)) + "\n") + + # Materials + fout.write("#\n# Materials\n#\n") + fout.write("materials=" + str(mydata.crt_mat) + "\n") + + fout.close() + self.report({'INFO'}, realpath + "successfully exported") + except: + self.report({'ERROR'}, "Unable to export room data") + + return {'FINISHED'} + + # ---------------------------------------------------------- + # Invoke + # ---------------------------------------------------------- + + # noinspection PyUnusedLocal + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} + + +# ---------------------------------------------------------- +# Import menu UI +# ---------------------------------------------------------- +class AchmImportRoom(bpy.types.Operator, ImportHelper): + bl_idname = "io_import.roomdata" + bl_description = 'Import Room data (.dat)' + bl_category = 'Archimesh' + bl_label = "Import" + + # From Helper. Filter filenames. + filename_ext = ".dat" + filter_glob = bpy.props.StringProperty( + default="*.dat", + options={'HIDDEN'}, + ) + + filepath = bpy.props.StringProperty( + name="File Path", + description="File path used for exporting room data file", + maxlen=1024, default="", + ) + + # ---------------------------------------------------------- + # Execute + # ---------------------------------------------------------- + # noinspection PyUnusedLocal + def execute(self, context): + print("Importing:", self.properties.filepath) + # noinspection PyBroadException + try: + realpath = os.path.realpath(os.path.expanduser(self.properties.filepath)) + finput = open(realpath) + line = finput.readline() + + myobj = bpy.context.active_object + mydata = myobj.RoomGenerator[0] + # ---------------------------------- + # Loop all records from file + # ---------------------------------- + idx = 0 # index of each wall + while line: + if line[:1] != '#': + if "name=" in line.lower(): + myobj.name = line[5:-1] + + elif "height=" in line.lower(): + mydata.room_height = float(line[7:-1]) + + elif "thickness=" in line.lower(): + mydata.wall_width = float(line[10:-1]) + + elif "inverse=" in line.lower(): + if line[8:-4].upper() == "T": + mydata.inverse = True + else: + mydata.inverse = False + + elif "ceiling=" in line.lower(): + if line[8:-4].upper() == "T": + mydata.ceiling = True + else: + mydata.ceiling = False + + elif "floor=" in line.lower(): + if line[6:-4].upper() == "T": + mydata.floor = True + else: + mydata.floor = False + + elif "close=" in line.lower(): + if line[6:-4].upper() == "T": + mydata.merge = True + else: + mydata.merge = False + elif "baseboard=" in line.lower(): + if line[10:-4].upper() == "T": + mydata.baseboard = True + else: + mydata.baseboard = False + elif "baseh=" in line.lower(): + mydata.base_height = float(line[6:-1]) + elif "baset=" in line.lower(): + mydata.base_width = float(line[6:-1]) + elif "shell=" in line.lower(): + if line[6:-4].upper() == "T": + mydata.shell = True + else: + mydata.shell = False + elif "shellh=" in line.lower(): + mydata.shell_height = float(line[7:-1]) + elif "shellt=" in line.lower(): + mydata.shell_thick = float(line[6:-1]) + elif "shellf=" in line.lower(): + mydata.shell_factor = float(line[6:-1]) + elif "shellb=" in line.lower(): + mydata.shell_bfactor = float(line[6:-1]) + elif "walls=" in line.lower(): + mydata.wall_num = int(line[6:-1]) + + # --------------------- + # Walls Data + # --------------------- + elif "w=" in line.lower() and idx < mydata.wall_num: + # get all pieces + buf = line[:-1] + "," + s = buf.split(",") + for e in s: + param = e.lower() + if "w=" in param: + mydata.walls[idx].w = float(e[2:]) + elif "a=" in param: + if "true" == param[2:]: + mydata.walls[idx].a = True + else: + mydata.walls[idx].a = False + elif "r=" in param: + mydata.walls[idx].r = float(e[2:]) + elif "h=" in param: + mydata.walls[idx].h = e[2:] + elif "m=" in param: + mydata.walls[idx].m = float(e[2:]) + elif "f=" == param[0:2]: + mydata.walls[idx].f = float(e[2:]) + elif "c=" in param: + if "true" == param[2:]: + mydata.walls[idx].curved = True + else: + mydata.walls[idx].curved = False + elif "cf=" in param: + mydata.walls[idx].curve_factor = float(e[3:]) + elif "cd=" in param: + mydata.walls[idx].curve_arc_deg = float(e[3:]) + elif "cs=" in param: + mydata.walls[idx].curve_steps = int(e[3:]) + idx += 1 + + elif "materials=" in line.lower(): + if line[10:-4].upper() == "T": + mydata.crt_mat = True + else: + mydata.crt_mat = False + + line = finput.readline() + + finput.close() + self.report({'INFO'}, realpath + "successfully imported") + except: + self.report({'ERROR'}, "Unable to import room data") + + return {'FINISHED'} + + # ---------------------------------------------------------- + # Invoke + # ---------------------------------------------------------- + # noinspection PyUnusedLocal + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} + + +# ------------------------------------------------------------------ +# Define operator class to create rooms +# ------------------------------------------------------------------ +class AchmRoom(bpy.types.Operator): + bl_idname = "mesh.archimesh_room" + bl_label = "Room" + bl_description = "Generate room with walls, baseboard, floor and ceiling" + bl_category = 'Archimesh' + bl_options = {'REGISTER', 'UNDO'} + + # ----------------------------------------------------- + # Draw (create UI interface) + # ----------------------------------------------------- + # noinspection PyUnusedLocal + def draw(self, context): + layout = self.layout + row = layout.row() + row.label("Use Properties panel (N) to define parms", icon='INFO') + row = layout.row(align=False) + row.operator("io_import.roomdata", text="Import", icon='COPYDOWN') + + # ----------------------------------------------------- + # Execute + # ----------------------------------------------------- + def execute(self, context): + if bpy.context.mode == "OBJECT": + create_room(self, context) + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archimesh: Option only valid in Object mode") + return {'CANCELLED'} + + +# ------------------------------------------------------------------------------ +# Create main object for the room. The other objects of room will be children of this. +# ------------------------------------------------------------------------------ +# noinspection PyUnusedLocal +def create_room(self, context): + # deselect all objects + for o in bpy.data.objects: + o.select = False + + # we create main object and mesh for walls + roommesh = bpy.data.meshes.new("Room") + roomobject = bpy.data.objects.new("Room", roommesh) + roomobject.location = bpy.context.scene.cursor_location + bpy.context.scene.objects.link(roomobject) + roomobject.RoomGenerator.add() + roomobject.RoomGenerator[0].walls.add() + + # we shape the walls and create other objects as children of 'RoomObject'. + shape_walls_and_create_children(roomobject, roommesh) + + # we select, and activate, main object for the room. + roomobject.select = True + bpy.context.scene.objects.active = roomobject + + +# ----------------------------------------------------- +# Verify if solidify exist +# ----------------------------------------------------- +def is_solidify(myobject): + flag = False + try: + if myobject.modifiers is None: + return False + + for mod in myobject.modifiers: + if mod.type == 'SOLIDIFY': + flag = True + break + return flag + except AttributeError: + return False + + +# ------------------------------------------------------------------------------ +# Update wall mesh and children objects (baseboard, floor and ceiling). +# ------------------------------------------------------------------------------ +# noinspection PyUnusedLocal +def update_room(self, context): + # When we update, the active object is the main object of the room. + o = bpy.context.active_object + oldmesh = o.data + oldname = o.data.name + # Now we deselect that room object to not delete it. + o.select = False + # and we create a new mesh for the walls: + tmp_mesh = bpy.data.meshes.new("temp") + # deselect all objects + for obj in bpy.data.objects: + obj.select = False + # Remove children created by this addon: + for child in o.children: + # noinspection PyBroadException + try: + if child["archimesh.room_object"]: + # noinspection PyBroadException + try: + # remove child relationship + for grandchild in child.children: + grandchild.parent = None + # remove modifiers + for mod in child.modifiers: + bpy.ops.object.modifier_remove(mod) + except: + pass + # clear data + old = child.data + child.select = True + bpy.ops.object.delete() + bpy.data.meshes.remove(old) + except: + pass + # Finally we create all that again (except main object), + shape_walls_and_create_children(o, tmp_mesh, True) + o.data = tmp_mesh + # Remove data (mesh of active object), + bpy.data.meshes.remove(oldmesh) + tmp_mesh.name = oldname + # and select, and activate, the main object of the room. + o.select = True + bpy.context.scene.objects.active = o + + +# ----------------------------------------------------- +# Move Solidify to Top +# ----------------------------------------------------- +def movetotopsolidify(myobject): + mymod = None + try: + if myobject.modifiers is not None: + for mod in myobject.modifiers: + if mod.type == 'SOLIDIFY': + mymod = mod + + if mymod is not None: + while myobject.modifiers[0] != mymod: + bpy.ops.object.modifier_move_up(modifier=mymod.name) + except AttributeError: + return + + +# ------------------------------------------------------------------------------ +# Generate walls, baseboard, floor, ceiling and materials. +# For walls, it only shapes mesh and creates modifier solidify (the modifier, only the first time). +# And, for the others, it creates object and mesh. +# ------------------------------------------------------------------------------ +def shape_walls_and_create_children(myroom, tmp_mesh, update=False): + rp = myroom.RoomGenerator[0] # "rp" means "room properties". + mybase = None + myfloor = None + myceiling = None + myshell = None + # Create the walls (only mesh, because the object is 'myRoom', created before). + create_walls(rp, tmp_mesh, get_blendunits(rp.room_height)) + # Mark Seams + select_vertices(myroom, [0, 1]) + mark_seam(myroom) + # Unwrap + unwrap_mesh(myroom) + + remove_doubles(myroom) + set_normals(myroom, not rp.inverse) # inside/outside + + if rp.wall_width > 0.0: + if update is False or is_solidify(myroom) is False: + set_modifier_solidify(myroom, get_blendunits(rp.wall_width)) + else: + for mod in myroom.modifiers: + if mod.type == 'SOLIDIFY': + mod.thickness = rp.wall_width + # Move to Top SOLIDIFY + movetotopsolidify(myroom) + + else: # clear not used SOLIDIFY + for mod in myroom.modifiers: + if mod.type == 'SOLIDIFY': + myroom.modifiers.remove(mod) + + # Create baseboard + if rp.baseboard: + baseboardmesh = bpy.data.meshes.new("Baseboard") + mybase = bpy.data.objects.new("Baseboard", baseboardmesh) + mybase.location = (0, 0, 0) + bpy.context.scene.objects.link(mybase) + mybase.parent = myroom + mybase.select = True + mybase["archimesh.room_object"] = True + mybase["archimesh.room_baseboard"] = True + + create_walls(rp, baseboardmesh, get_blendunits(rp.base_height), True) + set_normals(mybase, rp.inverse) # inside/outside room + if rp.base_width > 0.0: + set_modifier_solidify(mybase, get_blendunits(rp.base_width)) + # Move to Top SOLIDIFY + movetotopsolidify(mybase) + # Mark Seams + select_vertices(mybase, [0, 1]) + mark_seam(mybase) + # Unwrap + unwrap_mesh(mybase) + + # Create floor + if rp.floor and rp.wall_num > 1: + myfloor = create_floor(rp, "Floor", myroom) + myfloor["archimesh.room_object"] = True + myfloor.parent = myroom + # Unwrap + unwrap_mesh(myfloor) + + # Create ceiling + if rp.ceiling and rp.wall_num > 1: + myceiling = create_floor(rp, "Ceiling", myroom) + myceiling["archimesh.room_object"] = True + myceiling.parent = myroom + # Unwrap + unwrap_mesh(myceiling) + + # Create Shell + # + if rp.shell: + myshell = add_shell(myroom, "Wall_cover", rp) + myshell["archimesh.room_object"] = True + myshell["archimesh.room_shell"] = True + parentobject(myroom, myshell) + myshell.rotation_euler = myroom.rotation_euler + if rp.wall_width > 0.0: + # Solidify (need for boolean) + set_modifier_solidify(myshell, 0.01) + # Move to Top SOLIDIFY + movetotopsolidify(mybase) + + # Create materials + if rp.crt_mat: + # Wall material (two faces) + mat = create_diffuse_material("Wall_material", False, 0.765, 0.650, 0.588, 0.8, 0.621, 0.570, 0.1, True) + set_material(myroom, mat) + + # Baseboard material + if rp.baseboard and mybase is not None: + mat = create_diffuse_material("Baseboard_material", False, 0.8, 0.8, 0.8) + set_material(mybase, mat) + + # Ceiling material + if rp.ceiling and myceiling is not None: + mat = create_diffuse_material("Ceiling_material", False, 0.95, 0.95, 0.95) + set_material(myceiling, mat) + + # Floor material + if rp.floor and myfloor is not None: + mat = create_brick_material("Floor_material", False, 0.711, 0.668, 0.668, 0.8, 0.636, 0.315) + set_material(myfloor, mat) + + # Shell material + if rp.shell and myshell is not None: + mat = create_diffuse_material("Wall_cover_material", False, 0.507, 0.309, 0.076, 0.507, 0.309, 0.076) + set_material(myshell, mat) + + # deactivate others + for o in bpy.data.objects: + if o.select is True and o.name != myroom.name: + o.select = False + + +# ------------------------------------------------------------------------------ +# Create walls or baseboard (indicated with baseboard parameter). +# Some custom values are passed using the rp ("room properties" group) parameter (rp.myvariable). +# ------------------------------------------------------------------------------ +def create_walls(rp, mymesh, height, baseboard=False): + myvertex = [(0.0, 0.0, height), (0.0, 0.0, 0.0)] + myfaces = [] + lastface = 0 + lastx = lasty = 0 + idf = 0 + # Iterate the walls + for i in range(0, rp.wall_num): + if 0 == i: + prv = False + else: + prv = rp.walls[i - 1].a and not rp.walls[i - 1].curved + + mydat = make_wall(prv, rp.walls[i], baseboard, lastface, + lastx, lasty, height, myvertex, myfaces) + lastx = mydat[0] + lasty = mydat[1] + lastface = mydat[2] + + # -------------------------------------- + # saves vertex data for opengl + # -------------------------------------- + point_a = None + point_b = None + try: + for mf in myfaces[idf]: + if myvertex[mf][2] == 0: + if point_a is None: + point_a = myvertex[mf] + else: + point_b = myvertex[mf] + + rp.walls[i].glpoint_a = point_a + rp.walls[i].glpoint_b = point_b + except IndexError: + pass + + idf = len(myfaces) + + # Close room + if rp.merge is True: + if baseboard is False: + if rp.walls[rp.wall_num - 1].a is not True: + myfaces.extend([(0, 1, lastface + 1, lastface)]) + else: + if rp.walls[rp.wall_num - 1].curved is True: + myfaces.extend([(0, 1, lastface + 1, lastface)]) + else: + myfaces.extend([(0, 1, lastface, lastface + 1)]) + else: + myfaces.extend([(0, 1, lastface + 1, lastface)]) + + mymesh.from_pydata(myvertex, [], myfaces) + mymesh.update(calc_edges=True) + + +# ------------------------------------------------------------------------------ +# Make a Wall +# prv: If previous wall has 'curved' activate. +# lastFace: Number of faces of all before walls. +# lastX: X position of the end of the last wall. +# lastY: Y position of the end of the last wall. +# height: Height of the last wall, without peak. +# ------------------------------------------------------------------------------ +def make_wall(prv, wall, baseboard, lastface, lastx, lasty, height, myvertex, myfaces): + # size: Length of the wall. + # over: Height of the peak from "height". + # factor: Displacement of the peak (between -1 and 1; 0 is the middle of the wall). + advanced = wall.a + size = wall.w + over = wall.m + factor = wall.f + angle = wall.r + hide = wall.h + + # if angle negative, calculate real + # use add because the angle is negative + if angle < 0: + angle += 360 + # Verify Units + size = get_blendunits(size) + over = get_blendunits(over) + + # Calculate size using angle + sizex = math.cos(math.radians(angle)) * size + sizey = math.sin(math.radians(angle)) * size + + # Create faces + if advanced is False or baseboard is True: + # Cases of this first option: Baseboard or wall without peak and without curve. + if baseboard is True and advanced is True and wall.curved is True: + (myvertex, myfaces, sizex, sizey, lastface) = make_curved_wall(myvertex, myfaces, size, angle, + lastx, lasty, height, lastface, + wall.curve_factor, int(wall.curve_arc_deg), + int(wall.curve_arc_deg / wall.curve_steps), + hide, baseboard) + else: + myvertex.extend([(lastx + sizex, lasty + sizey, height), + (lastx + sizex, lasty + sizey, 0.0)]) + if check_visibility(hide, baseboard): + if prv is False or baseboard is True: + # Previous no advance or advance with curve + myfaces.extend([(lastface, lastface + 2, lastface + 3, lastface + 1)]) + else: + # Previous advance without curve + myfaces.extend([(lastface, lastface + 1, lastface + 2, lastface + 3)]) + lastface += 2 + else: + # Case of this second option: Wall with advanced features (orientation, visibility and peak or curve). + # Orientation and visibility options ('angle' and 'hide' variables) are only visible in panel + # with advanced features, but are taken in account in any case. + if wall.curved: + # Wall with curve and without peak. + (myvertex, myfaces, sizex, sizey, lastface) = make_curved_wall(myvertex, myfaces, size, angle, + lastx, lasty, height, lastface, + wall.curve_factor, int(wall.curve_arc_deg), + int(wall.curve_arc_deg / wall.curve_steps), + hide, baseboard) + else: + # Wall with peak and without curve. + mid = size / 2 + ((size / 2) * factor) + midx = math.cos(math.radians(angle)) * mid + midy = math.sin(math.radians(angle)) * mid + # first face + myvertex.extend([(lastx + midx, lasty + midy, height + over), + (lastx + midx, lasty + midy, 0.0)]) + if check_visibility(hide, baseboard): + if math.fabs(factor) != 1: + if prv is False: + # Previous no advance or advance with curve + myfaces.extend([(lastface, lastface + 2, lastface + 3, lastface + 1)]) + else: + # Previous advance without curve + myfaces.extend([(lastface, lastface + 1, lastface + 2, lastface + 3)]) + # second face + myvertex.extend([(lastx + sizex, lasty + sizey, 0.0), + (lastx + sizex, lasty + sizey, height)]) + if check_visibility(hide, baseboard): + if math.fabs(factor) != 1: + myfaces.extend([(lastface + 2, lastface + 3, lastface + 4, lastface + 5)]) + else: + if prv is False: + myfaces.extend([(lastface, lastface + 5, lastface + 4, lastface + 1), + (lastface, lastface + 2, lastface + 5)]) + else: + myfaces.extend([(lastface, lastface + 4, lastface + 5, lastface + 1), + (lastface + 1, lastface + 2, lastface + 5)]) + + lastface += 4 + + lastx += sizex + lasty += sizey + + return lastx, lasty, lastface + + +# ------------------------------------------------------------------------------ +# Verify visibility of walls +# ------------------------------------------------------------------------------ +def check_visibility(h, base): + # Visible + if h == '0': + return True + # Wall + if h == '2': + if base is True: + return False + else: + return True + # Baseboard + if h == '1': + if base is True: + return True + else: + return False + # Hidden + if h == '3': + return False + + +# ------------------------------------------------------------------------------ +# Create a curved wall. +# ------------------------------------------------------------------------------ +def make_curved_wall(myvertex, myfaces, size, wall_angle, lastx, lasty, height, + lastface, curve_factor, arc_angle, step_angle, hide, baseboard): + curvex = None + curvey = None + # Calculate size using angle + sizex = math.cos(math.radians(wall_angle)) * size + sizey = math.sin(math.radians(wall_angle)) * size + + for step in range(0, arc_angle + step_angle, step_angle): + curvex = sizex / 2 - math.cos(math.radians(step + wall_angle)) * size / 2 + curvey = sizey / 2 - math.sin(math.radians(step + wall_angle)) * size / 2 + curvey = curvey * curve_factor + myvertex.extend([(lastx + curvex, lasty + curvey, height), + (lastx + curvex, lasty + curvey, 0.0)]) + if check_visibility(hide, baseboard): + myfaces.extend([(lastface, lastface + 2, lastface + 3, lastface + 1)]) + lastface += 2 + return myvertex, myfaces, curvex, curvey, lastface + + +# ------------------------------------------------------------------------------ +# Create floor or ceiling (create object and mesh) +# Parameters: +# rm: "room properties" group +# typ: Name of new object and mesh ('Floor' or 'Ceiling') +# myRoom: Main object for the room +# ------------------------------------------------------------------------------ + +def create_floor(rp, typ, myroom): + bpy.context.scene.objects.active = myroom + + myvertex = [] + myfaces = [] + verts = [] + + obverts = bpy.context.active_object.data.vertices + for vertex in obverts: + verts.append(tuple(vertex.co)) + # Loop only selected + i = 0 + for e in verts: + if typ == "Floor": + if e[2] == 0.0: + myvertex.extend([(e[0], e[1], e[2])]) + i += 1 + else: # ceiling + if round(e[2], 5) == round(get_blendunits(rp.room_height), 5): + myvertex.extend([(e[0], e[1], e[2])]) + i += 1 + + # Create faces + fa = [] + for f in range(0, i): + fa.extend([f]) + + myfaces.extend([fa]) + + mymesh = bpy.data.meshes.new(typ) + myobject = bpy.data.objects.new(typ, mymesh) + + myobject.location = (0, 0, 0) + bpy.context.scene.objects.link(myobject) + + mymesh.from_pydata(myvertex, [], myfaces) + mymesh.update(calc_edges=True) + + return myobject + + +# ------------------------------------------------------------------ +# Define property group class to create, or modify, room walls. +# ------------------------------------------------------------------ +class WallProperties(bpy.types.PropertyGroup): + w = bpy.props.FloatProperty( + name='Length', + min=-150, max=150, + default=1, precision=3, + description='Length of the wall (negative to reverse direction)', + update=update_room, + ) + + a = bpy.props.BoolProperty( + name="Advance", + description="Define advance parameters of the wall", + default=False, + update=update_room, + ) + + curved = bpy.props.BoolProperty( + name="Curved", + description="Enable curved wall parameters", + default=False, + update=update_room, + ) + curve_factor = bpy.props.FloatProperty( + name='Factor', + min=-5, max=5, + default=1, precision=1, + description='Curvature variation', + update=update_room, + ) + curve_arc_deg = bpy.props.FloatProperty( + name='Degrees', min=1, max=359, + default=180, precision=1, + description='Degrees of the curve arc (must be >= steps)', + update=update_room, + ) + curve_steps = bpy.props.IntProperty( + name='Steps', + min=2, max=50, + default=12, + description='Curve steps', + update=update_room, + ) + + m = bpy.props.FloatProperty( + name='Peak', min=0, max=50, + default=0, precision=3, + description='Middle height variation', + update=update_room, + ) + f = bpy.props.FloatProperty( + name='Factor', min=-1, max=1, + default=0, precision=3, + description='Middle displacement', + update=update_room, + ) + r = bpy.props.FloatProperty( + name='Angle', + min=-180, max=180, + default=0, precision=1, + description='Wall Angle (-180 to +180)', + update=update_room, + ) + + h = bpy.props.EnumProperty( + items=( + ('0', "Visible", ""), + ('1', "Baseboard", ""), + ('2', "Wall", ""), + ('3', "Hidden", ""), + ), + name="", + description="Wall visibility", + update=update_room, + ) + + # opengl internal data + glpoint_a = bpy.props.FloatVectorProperty( + name="glpointa", + description="Hidden property for opengl", + default=(0, 0, 0), + ) + glpoint_b = bpy.props.FloatVectorProperty( + name="glpointb", + description="Hidden property for opengl", + default=(0, 0, 0), + ) + +bpy.utils.register_class(WallProperties) + + +# ------------------------------------------------------------------ +# Add a new room wall. +# First add a parameter group for that new wall, and then update the room. +# ------------------------------------------------------------------ +def add_room_wall(self, context): + rp = context.object.RoomGenerator[0] + for cont in range(len(rp.walls) - 1, rp.wall_num): + rp.walls.add() + # by default, we alternate the direction of the walls. + if 1 == cont % 2: + rp.walls[cont].r = 90 + update_room(self, context) + + +# ------------------------------------ +# Get if some vertex is highest +# ------------------------------------ +def get_hight(verts, faces_4, faces_3, face_index, face_num): + rtn = face_index + a = faces_4[face_num][0] + b = faces_4[face_num][1] + c = faces_4[face_num][2] + d = faces_4[face_num][3] + + for face3 in faces_3: + for idx3 in face3: + if idx3 != face_index: + # check x and y position (must be equal) + if verts[idx3][0] == verts[face_index][0] and verts[idx3][1] == verts[face_index][1]: + # only if z is > that previous z + if verts[idx3][2] > verts[face_index][2]: + # checking if the original vertex is in the same face + # must have 2 vertices on the original face + t = 0 + for e in face3: + if e == a or e == b or e == c or e == d: + t += 1 + if t >= 2: + rtn = idx3 + + return rtn + + +# ------------------------------------ +# Sort list of faces +# ------------------------------------ +def sort_facelist(activefaces, activenormals): + totfaces = len(activefaces) + newlist = [] + newnormal = [] + # ----------------------- + # Only one face + # ----------------------- + if totfaces == 1: + newlist.append(activefaces[0]) + newnormal.append(activenormals[0]) + return newlist, newnormal + + # ----------------------- + # Look for first element + # ----------------------- + flag = False + for x in range(1, totfaces): + if flag is False: + idx = 0 + for face in activefaces: + c = 0 + for i in face: + if i == 0 or i == 1: + c += 1 + # avoid close + if i > 3: + c -= 1 + + if c >= 2 and face not in newlist: + newlist.append(face) + newnormal.append(activenormals[idx]) + flag = True + idx += 1 + + # ----------------------- + # Look for second element + # ----------------------- + flag = False + for x in range(1, totfaces): + if flag is False: + idx = 0 + for face in activefaces: + c = 0 + for i in face: + if i == 2 or i == 3: + c += 1 + if c >= 2 and face not in newlist: + newlist.append(face) + newnormal.append(activenormals[idx]) + flag = True + idx += 1 + + # ----------------------- + # Add next faces + # ----------------------- + for x in range(2, totfaces): + idx = 0 + for face in activefaces: + c = 0 + for i in face: + if i == newlist[x - 1][0] or i == newlist[x - 1][1] or i == newlist[x - 1][2] or i == newlist[x - 1][3]: + c += 1 + if c >= 2 and face not in newlist: + newlist.append(face) + newnormal.append(activenormals[idx]) + idx += 1 + + return newlist, newnormal + + +# ------------------------------------ +# Get points of the walls +# selobject: room +# ------------------------------------ +def get_wall_points(selobject): + obverts = selobject.data.vertices + obfaces = selobject.data.polygons + + verts = [] + faces_3 = [] + faces_4 = [] + normals = [] + activefaces = [] + activenormals = [] + + # -------------------------- + # Recover all vertex + # -------------------------- + for vertex in obverts: + verts.append(list(vertex.co)) + + # -------------------------- + # Recover 3 faces + # -------------------------- + for face in obfaces: + # get only 4 corners faces + if len(list(face.vertices)) == 3: + faces_3.append(list(face.vertices)) + # -------------------------- + # Recover 4 faces + # -------------------------- + for face in obfaces: + # get only 4 corners faces + if len(list(face.vertices)) == 4: + faces_4.append(list(face.vertices)) + normals.append(face.normal) + # -------------------------- + # Replace highest + # -------------------------- + idx = 0 + for face in faces_4: + mylist = [] + for e in face: # e contains the number of vertex element + if verts[e][2] == 0: + mylist.append(e) + # Only if Z > 0, recalculate + if verts[e][2] != 0: + mylist.append(get_hight(verts, faces_4, faces_3, e, idx)) + + activefaces.append(mylist) + activenormals.append(normals[idx]) + idx += 1 + + # ------------------------ + # Sort faces + # ------------------------ + newlist, newnormal = sort_facelist(activefaces, activenormals) + return verts, newlist, newnormal + + +# ------------------------------------ +# Create a shell of boards +# selobject: room +# objname: Name for new object +# rp: room properties +# ------------------------------------ +def add_shell(selobject, objname, rp): + + myvertex = [] + myfaces = [] + + verts, activefaces, activenormals = get_wall_points(selobject) + + # -------------------------- + # Get line points + # -------------------------- + i = 0 + idx = 0 + for face in activefaces: + a1 = None + b1 = None + a2 = None + b2 = None + # Bottom + for e in face: + if verts[e][2] == 0: + if a1 is None: + a1 = e + else: + b1 = e + # Top + for e in face: + if verts[e][2] != 0: + if verts[a1][0] == verts[e][0] and verts[a1][1] == verts[e][1]: + a2 = e + else: + b2 = e + # Create the mesh + mydata = create_cover_mesh(idx, verts, activefaces, activenormals, i, a1, a2, b1, b2, + rp.merge, 0.005, + rp.shell_height, rp.shell_thick, rp.shell_factor, rp.shell_bfactor) + i = mydata[0] + myvertex.extend(mydata[1]) + myfaces.extend(mydata[2]) + idx += 1 + # -------------------------- + # Create the mesh + # -------------------------- + mesh = bpy.data.meshes.new(objname) + myobject = bpy.data.objects.new(objname, mesh) + + myobject.location = selobject.location + bpy.context.scene.objects.link(myobject) + + mesh.from_pydata(myvertex, [], myfaces) + mesh.update(calc_edges=True) + + remove_doubles(myobject) + set_normals(myobject) + + return myobject + + +# --------------------------------------------------------- +# Project point using face normals +# +# m: Magnitud +# pf: Comparision face +/- +# --------------------------------------------------------- +def project_point(idx, point, normals, m, pf): + v1 = mathutils.Vector(normals[idx]) + if idx + pf >= len(normals): + vf = v1 + elif idx + pf < 0: + vf = v1 + else: + v2 = mathutils.Vector(normals[idx + pf]) + if v1 != v2: + vf = v1 + v2 + vf.normalize() # must be length equal to 1 + else: + vf = v1 + + n1 = (vf[0] * m, vf[1] * m, vf[2] * m) + p1 = (point[0] + n1[0], point[1] + n1[1], point[2] + n1[2]) + return p1 + + +# --------------------------------------------------------- +# Create wall cover mesh +# +# Uses linear equation for cutting +# +# Z = This value is the z axis value +# so, we can replace t with ((Z-Z1) / (Z2-Z1)) +# +# X = X1 + ((X2 - X1) * t) +# +# X = X1 + ((X2 - X1) * ((Z-Z1) / (Z2-Z1))) +# Y = Y1 + ((Y2 - Y1) * ((Z-Z1) / (Z2-Z1))) +# +# height refers to the height of the cover piece +# width refers to the width of the cover piece +# --------------------------------------------------------- + + +def create_cover_mesh(idx, verts, activefaces, normals, i, a1, a2, b1, b2, merge, space=0.005, + height=0.20, thickness=0.025, shell_factor=1, shell_bfactor=1): + pvertex = [] + pfaces = [] + + a1_x = verts[a1][0] + a1_y = verts[a1][1] + a1_z = verts[a1][2] + + a2_x = verts[a2][0] + a2_y = verts[a2][1] + a2_z = verts[a2][2] + + b1_x = verts[b1][0] + b1_y = verts[b1][1] + b1_z = verts[b1][2] + + b2_x = verts[b2][0] + b2_y = verts[b2][1] + b2_z = verts[b2][2] + + # Get highest + if a2_z >= b2_z: + top = a2_z + limit = b2_z + else: + top = b2_z + limit = a2_z + + # apply factor + # get high point of walls + maxh = 0 + for v in verts: + if v[2] > maxh: + maxh = v[2] + maxh *= shell_factor + minh = maxh * (1 - shell_bfactor) + if minh < 0: + minh = 0 + + if shell_factor < 1: + if top > maxh: + top = maxh + + # -------------------------------------- + # Loop to generate each piece of cover + # -------------------------------------- + zpos = minh # initial position + f = 0 + f2 = 0 + # detect what face must use to compare + face_num = len(activefaces) - 1 + if idx == 0 and merge is True: + if is_in_nextface(idx + 1, activefaces, verts, a1_x, a1_y) is True: + side_a = 1 + side_b = face_num + else: + side_a = face_num + side_b = 1 + elif idx == face_num and merge is True: + if is_in_nextface(face_num, activefaces, verts, a1_x, a1_y) is False: + side_b = -face_num + side_a = -1 + else: + side_b = -1 + side_a = -face_num + else: + if is_in_nextface(idx + 1, activefaces, verts, a1_x, a1_y) is True: + side_a = 1 + side_b = -1 + else: + side_a = -1 + side_b = 1 + # Last wall + if idx + 1 >= len(activefaces): + if is_in_nextface(idx - 1, activefaces, verts, a1_x, a1_y) is True: + side_a = -1 + side_b = 1 + else: + side_a = 1 + side_b = -1 + + na1_x = 0 + na1_y = 0 + na2_x = 0 + na2_y = 0 + + nb1_x = 0 + nb1_y = 0 + nb2_x = 0 + nb2_y = 0 + + nc1_x = 0 + nc1_y = 0 + nc2_x = 0 + nc2_y = 0 + + nd1_x = 0 + nd1_y = 0 + nd2_x = 0 + nd2_y = 0 + + while zpos <= top: + # ---------------------- + # Full cover piece + # ---------------------- + if zpos <= limit: + # ---------------- + # Point A + # ---------------- + mypoint = project_point(idx, (a1_x, a1_y, zpos), normals, space, side_a) + + pvertex.extend([mypoint]) + na1_x = mypoint[0] + na1_y = mypoint[1] + # external point + mypoint = project_point(idx, (a1_x, a1_y, zpos), normals, space + thickness, side_a) + pvertex.extend([mypoint]) + nc1_x = mypoint[0] + nc1_y = mypoint[1] + # get second point (vertical) + mypoint = project_point(idx, (a2_x, a2_y, zpos), normals, space, side_a) + na2_x = mypoint[0] + na2_y = mypoint[1] + mypoint = project_point(idx, (a2_x, a2_y, zpos), normals, space + thickness, side_a) + nc2_x = mypoint[0] + nc2_y = mypoint[1] + + # ---------------- + # Point B + # ---------------- + mypoint = project_point(idx, (b1_x, b1_y, zpos), normals, space, side_b) + pvertex.extend([mypoint]) + nb1_x = mypoint[0] + nb1_y = mypoint[1] + # external point + mypoint = project_point(idx, (b1_x, b1_y, zpos), normals, space + thickness, side_b) + pvertex.extend([mypoint]) + nd1_x = mypoint[0] + nd1_y = mypoint[1] + # get second point (vertical) + mypoint = project_point(idx, (b2_x, b2_y, zpos), normals, space, side_b) + nb2_x = mypoint[0] + nb2_y = mypoint[1] + mypoint = project_point(idx, (b2_x, b2_y, zpos), normals, space + thickness, side_b) + nd2_x = mypoint[0] + nd2_y = mypoint[1] + + # Faces + if zpos != top: + pfaces.extend([(i, i + 1, i + 3, i + 2)]) + + if f >= 1: + pfaces.extend([(i - 3, i, i + 2, i - 1)]) + + i += 4 + f += 1 + # ---------------------- + # Cut pieces + # ---------------------- + else: + # ------------------------------- + # Internal Points + # ------------------------------- + # Get highest + if a2_z >= b2_z: + ax1 = na1_x + ay1 = na1_y + az1 = a1_z + ax2 = na2_x + ay2 = na2_y + az2 = a2_z + + bx1 = na2_x + by1 = na2_y + bz1 = a2_z + bx2 = nb2_x + by2 = nb2_y + bz2 = b2_z + else: + ax1 = na2_x + ay1 = na2_y + az1 = a2_z + ax2 = nb2_x + ay2 = nb2_y + az2 = b2_z + + bx1 = nb1_x + by1 = nb1_y + bz1 = b1_z + bx2 = nb2_x + by2 = nb2_y + bz2 = b2_z + + # ---------------- + # Point A + # ---------------- + x = ax1 + ((ax2 - ax1) * ((zpos - az1) / (az2 - az1))) + y = ay1 + ((ay2 - ay1) * ((zpos - az1) / (az2 - az1))) + pvertex.extend([(x, y, zpos)]) + # ---------------- + # Point B + # ---------------- + x = bx1 + ((bx2 - bx1) * ((zpos - bz1) / (bz2 - bz1))) + y = by1 + ((by2 - by1) * ((zpos - bz1) / (bz2 - bz1))) + pvertex.extend([(x, y, zpos)]) + # ------------------------------- + # External Points + # ------------------------------- + # Get highest + if a2_z >= b2_z: + ax1 = nc1_x + ay1 = nc1_y + az1 = a1_z + ax2 = nc2_x + ay2 = nc2_y + az2 = a2_z + + bx1 = nc2_x + by1 = nc2_y + bz1 = a2_z + bx2 = nd2_x + by2 = nd2_y + bz2 = b2_z + else: + ax1 = nc2_x + ay1 = nc2_y + az1 = a2_z + ax2 = nd2_x + ay2 = nd2_y + az2 = b2_z + + bx1 = nd1_x + by1 = nd1_y + bz1 = b1_z + bx2 = nd2_x + by2 = nd2_y + bz2 = b2_z + + # ---------------- + # Point A + # ---------------- + x = ax1 + ((ax2 - ax1) * ((zpos - az1) / (az2 - az1))) + y = ay1 + ((ay2 - ay1) * ((zpos - az1) / (az2 - az1))) + pvertex.extend([(x, y, zpos)]) + # ---------------- + # Point B + # ---------------- + x = bx1 + ((bx2 - bx1) * ((zpos - bz1) / (bz2 - bz1))) + y = by1 + ((by2 - by1) * ((zpos - bz1) / (bz2 - bz1))) + pvertex.extend([(x, y, zpos)]) + # Faces + if zpos != top: + pfaces.extend([(i, i + 1, i + 3, i + 2)]) + + if f2 == 0: + pfaces.extend([(i - 1, i - 3, i, i + 1)]) + else: + pfaces.extend([(i - 1, i - 2, i, i + 1)]) + + i += 4 + f2 += 1 + # avoid infinite loop + if zpos == top: + break + + # add new piece + zpos += height + # cut oversized + if zpos > top: + zpos = top + + return i, pvertex, pfaces + + +# ------------------------------------------------------------- +# Detect if the vertex is face +# ------------------------------------------------------------- +def is_in_nextface(idx, activefaces, verts, x, y): + if idx >= len(activefaces): + return False + + for f in activefaces[idx]: + if verts[f][2] == 0: # only ground + if verts[f][0] == x and verts[f][1] == y: + return True + + return False + + +# ------------------------------------------------------------------ +# Define property group class to create or modify a rooms. +# ------------------------------------------------------------------ +class RoomProperties(bpy.types.PropertyGroup): + room_height = bpy.props.FloatProperty( + name='Height', min=0.001, max=50, + default=2.4, precision=3, + description='Room height', update=update_room, + ) + wall_width = bpy.props.FloatProperty( + name='Thickness', min=0.000, max=10, + default=0.0, precision=3, + description='Thickness of the walls', update=update_room, + ) + inverse = bpy.props.BoolProperty( + name="Inverse", description="Inverse normals to outside", + default=False, + update=update_room, + ) + crt_mat = bpy.props.BoolProperty( + name="Create default Cycles materials", + description="Create default materials for Cycles render", + default=True, + update=update_room, + ) + + wall_num = bpy.props.IntProperty( + name='Number of Walls', min=1, max=50, + default=1, + description='Number total of walls in the room', update=add_room_wall, + ) + + baseboard = bpy.props.BoolProperty( + name="Baseboard", description="Create a baseboard automatically", + default=True, + update=update_room, + ) + + base_width = bpy.props.FloatProperty( + name='Width', min=0.001, max=10, + default=0.015, precision=3, + description='Baseboard width', update=update_room, + ) + base_height = bpy.props.FloatProperty( + name='Height', min=0.05, max=20, + default=0.12, precision=3, + description='Baseboard height', update=update_room, + ) + + ceiling = bpy.props.BoolProperty( + name="Ceiling", description="Create a ceiling", + default=False, update=update_room, + ) + floor = bpy.props.BoolProperty( + name="Floor", description="Create a floor automatically", + default=False, + update=update_room, + ) + + merge = bpy.props.BoolProperty( + name="Close walls", description="Close walls to create a full closed room", + default=False, update=update_room, + ) + + walls = bpy.props.CollectionProperty( + type=WallProperties, + ) + + shell = bpy.props.BoolProperty( + name="Wall cover", description="Create a cover of boards", + default=False, update=update_room, + ) + shell_thick = bpy.props.FloatProperty( + name='Thickness', min=0.001, max=1, + default=0.025, precision=3, + description='Cover board thickness', update=update_room, + ) + shell_height = bpy.props.FloatProperty( + name='Height', min=0.05, max=1, + default=0.20, precision=3, + description='Cover board height', update=update_room, + ) + shell_factor = bpy.props.FloatProperty( + name='Top', min=0.1, max=1, + default=1, precision=1, + description='Percentage for top covering (1 Full)', update=update_room, + ) + shell_bfactor = bpy.props.FloatProperty( + name='Bottom', min=0.1, max=1, + default=1, precision=1, + description='Percentage for bottom covering (1 Full)', update=update_room, + ) + +bpy.utils.register_class(RoomProperties) +bpy.types.Object.RoomGenerator = bpy.props.CollectionProperty(type=RoomProperties) + + +# ----------------------------------------------------- +# Add wall parameters to the panel. +# ----------------------------------------------------- +def add_wall(idx, box, wall): + box.label("Wall " + str(idx)) + row = box.row() + row.prop(wall, 'w') + row.prop(wall, 'a') + # row.prop(wall, 'curved') + if wall.a is True: + srow = box.row() + srow.prop(wall, 'r') + srow.prop(wall, 'h') + + srow = box.row() + srow.prop(wall, 'curved') + + if wall.curved is False: + srow.prop(wall, 'm') + srow.prop(wall, 'f') + + if wall.curved is True: + srow.prop(wall, 'curve_factor') + srow.prop(wall, 'curve_arc_deg') + srow.prop(wall, 'curve_steps') + + +# ------------------------------------------------------------------ +# Define panel class to modify rooms. +# ------------------------------------------------------------------ +class AchmRoomGeneratorPanel(bpy.types.Panel): + bl_idname = "OBJECT_PT_room_generator" + bl_label = "Room" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = 'Archimesh' + + # ----------------------------------------------------- + # Verify if visible + # ----------------------------------------------------- + @classmethod + def poll(cls, context): + o = context.object + if o is None: + return False + if 'RoomGenerator' not in o: + return False + else: + return True + + # ----------------------------------------------------- + # Draw (create UI interface) + # ----------------------------------------------------- + def draw(self, context): + o = context.object + # If the selected object didn't be created with the group 'RoomGenerator', this panel is not created. + # noinspection PyBroadException + try: + if 'RoomGenerator' not in o: + return + except: + return + + layout = self.layout + if bpy.context.mode == 'EDIT_MESH': + layout.label('Warning: Operator does not work in edit mode.', icon='ERROR') + else: + room = o.RoomGenerator[0] + row = layout.row() + row.prop(room, 'room_height') + row.prop(room, 'wall_width') + row.prop(room, 'inverse') + + row = layout.row() + if room.wall_num > 1: + row.prop(room, 'ceiling') + row.prop(room, 'floor') + row.prop(room, 'merge') + + # Wall number + row = layout.row() + row.prop(room, 'wall_num') + + # Add menu for walls + if room.wall_num > 0: + for wall_index in range(0, room.wall_num): + box = layout.box() + add_wall(wall_index + 1, box, room.walls[wall_index]) + + box = layout.box() + box.prop(room, 'baseboard') + if room.baseboard is True: + row = box.row() + row.prop(room, 'base_width') + row.prop(room, 'base_height') + + box = layout.box() + box.prop(room, 'shell') + if room.shell is True: + row = box.row() + row.prop(room, 'shell_height') + row.prop(room, 'shell_thick') + row = box.row() + row.prop(room, 'shell_factor', slider=True) + row.prop(room, 'shell_bfactor', slider=True) + + box = layout.box() + box.prop(room, 'crt_mat') |