# SPDX-License-Identifier: GPL-2.0-or-later # ---------------------------------------------------------- # Automatic generation of rooms # Author: Antonio Vazquez (antonioya) and Eduardo Gutierrez # # ---------------------------------------------------------- # noinspection PyUnresolvedReferences import bpy from math import sin, cos, fabs, radians from mathutils import Vector from datetime import datetime from time import time from os import path from bpy.types import Operator, PropertyGroup, Object, Panel from bpy.props import StringProperty, FloatProperty, BoolProperty, IntProperty, FloatVectorProperty, \ CollectionProperty, EnumProperty from bpy_extras.io_utils import ExportHelper, ImportHelper from .achm_tools import * # ---------------------------------------------------------- # Export menu UI # ---------------------------------------------------------- class ARCHIMESH_OT_ExportRoom(Operator, ExportHelper): bl_idname = "io_export.roomdata" bl_description = 'Export Room data (.dat)' bl_category = 'View' bl_label = "Export" # From ExportHelper. Filter filenames. filename_ext = ".dat" filter_glob: StringProperty( default="*.dat", options={'HIDDEN'}, ) filepath: 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) = path.split(self.properties.filepath) print('Exporting %s' % filename) # ------------------------------- # Open output file # ------------------------------- realpath = path.realpath(path.expanduser(self.properties.filepath)) fout = open(realpath, 'w') st = datetime.fromtimestamp(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 ARCHIMESH_OT_ImportRoom(Operator, ImportHelper): bl_idname = "io_import.roomdata" bl_description = 'Import Room data (.dat)' bl_category = 'View' bl_label = "Import" # From Helper. Filter filenames. filename_ext = ".dat" filter_glob: StringProperty( default="*.dat", options={'HIDDEN'}, ) filepath: 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 = path.realpath(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 ARCHIMESH_OT_Room(Operator): bl_idname = "mesh.archimesh_room" bl_label = "Room" bl_description = "Generate room with walls, baseboard, floor and ceiling" bl_category = 'View' bl_options = {'REGISTER', 'UNDO'} # ----------------------------------------------------- # Draw (create UI interface) # ----------------------------------------------------- # noinspection PyUnusedLocal def draw(self, context): layout = self.layout row = layout.row() row.label(text="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_set(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.collection.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. bpy.context.view_layer.objects.active = roomobject roomobject.select_set(True) # ----------------------------------------------------- # 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_set(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_set(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_set(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_set(True) bpy.context.view_layer.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)) myroom.data = tmp_mesh # 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.collection.objects.link(mybase) mybase.parent = myroom mybase.select_set(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: 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 and bpy.context.scene.render.engine in {'CYCLES', 'BLENDER_EEVEE'}: # 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_get() is True and o.name != myroom.name: o.select_set(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 = cos(radians(angle)) * size sizey = sin(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 = cos(radians(angle)) * mid midy = sin(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 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 fabs(factor) != 1: myfaces.extend([(lastface + 2, lastface + 5, lastface + 4, lastface + 3)]) 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 = cos(radians(wall_angle)) * size sizey = sin(radians(wall_angle)) * size for step in range(0, arc_angle + step_angle, step_angle): curvex = sizex / 2 - cos(radians(step + wall_angle)) * size / 2 curvey = sizey / 2 - sin(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.view_layer.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.collection.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(PropertyGroup): w: FloatProperty( name='Length', min=-150, max=150, default=1, precision=3, description='Length of the wall (negative to reverse direction)', update=update_room, ) a: BoolProperty( name="Advanced", description="Define advanced parameters of the wall", default=False, update=update_room, ) curved: BoolProperty( name="Curved", description="Enable curved wall parameters", default=False, update=update_room, ) curve_factor: FloatProperty( name='Factor', min=-5, max=5, default=1, precision=1, description='Curvature variation', update=update_room, ) curve_arc_deg: 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: IntProperty( name='Steps', min=2, max=50, default=12, description='Curve steps', update=update_room, ) m: FloatProperty( name='Peak', min=0, max=50, default=0, precision=3, description='Middle height variation', update=update_room, ) f: FloatProperty( name='Factor', min=-1, max=1, default=0, precision=3, description='Middle displacement', update=update_room, ) r: FloatProperty( name='Angle', min=-180, max=180, default=0, precision=1, description='Wall Angle (-180 to +180)', update=update_room, ) h: EnumProperty( items=( ('0', "Visible", ""), ('1', "Baseboard", ""), ('2', "Wall", ""), ('3', "Hidden", ""), ), name="", description="Wall visibility", update=update_room, ) # opengl internal data glpoint_a: FloatVectorProperty( name="glpointa", description="Hidden property for opengl", default=(0, 0, 0), ) glpoint_b: 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 # ----------------------- idx = 0 for face in activefaces: c = 0 for i in face: if i == 0 or i == 1: c += 1 if c >= 2 and face not in newlist: newlist.append(face) newnormal.append(activenormals[idx]) break idx += 1 # ----------------------- # Look for second element # ----------------------- 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]) break 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.collection.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: Comparison face +/- # --------------------------------------------------------- def project_point(idx, point, normals, m, pf): v1 = Vector(normals[idx]) if idx + pf >= len(normals): vf = v1 elif idx + pf < 0: vf = v1 else: v2 = 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(PropertyGroup): room_height: FloatProperty( name='Height', min=0.001, max=50, default=2.4, precision=3, description='Room height', update=update_room, ) wall_width: FloatProperty( name='Thickness', min=0.000, max=10, default=0.0, precision=3, description='Thickness of the walls', update=update_room, ) inverse: BoolProperty( name="Inverse", description="Inverse normals to outside", default=False, update=update_room, ) crt_mat: BoolProperty( name="Create default Cycles materials", description="Create default materials for Cycles render", default=True, update=update_room, ) wall_num: IntProperty( name='Number of Walls', min=1, max=50, default=1, description='Number total of walls in the room', update=add_room_wall, ) baseboard: BoolProperty( name="Baseboard", description="Create a baseboard automatically", default=True, update=update_room, ) base_width: FloatProperty( name='Width', min=-10, max=10, default=0.015, precision=3, description='Baseboard width', update=update_room, ) base_height: FloatProperty( name='Height', min=0.05, max=20, default=0.12, precision=3, description='Baseboard height', update=update_room, ) ceiling: BoolProperty( name="Ceiling", description="Create a ceiling", default=False, update=update_room, ) floor: BoolProperty( name="Floor", description="Create a floor automatically", default=False, update=update_room, ) merge: BoolProperty( name="Close walls", description="Close walls to create a full closed room", default=False, update=update_room, ) walls: CollectionProperty( type=WallProperties, ) shell: BoolProperty( name="Wall cover", description="Create a cover of boards", default=False, update=update_room, ) shell_thick: FloatProperty( name='Thickness', min=0.001, max=1, default=0.025, precision=3, description='Cover board thickness', update=update_room, ) shell_height: FloatProperty( name='Height', min=0.05, max=1, default=0.20, precision=3, description='Cover board height', update=update_room, ) shell_factor: FloatProperty( name='Top', min=0.1, max=1, default=1, precision=1, description='Percentage for top covering (1 Full)', update=update_room, ) shell_bfactor: 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) Object.RoomGenerator = CollectionProperty(type=RoomProperties) # ----------------------------------------------------- # Add wall parameters to the panel. # ----------------------------------------------------- def add_wall(idx, box, wall): box.label(text="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 ARCHIMESH_PT_RoomGenerator(Panel): bl_idname = "OBJECT_PT_room_generator" bl_label = "Room" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = 'Create' # ----------------------------------------------------- # 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(text='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() if not context.scene.render.engine in {'CYCLES', 'BLENDER_EEVEE'}: box.enabled = False box.prop(room, 'crt_mat')