# SPDX-License-Identifier: GPL-2.0-or-later # ---------------------------------------------------------- # Automatic generation of stairs # Author: Antonio Vazquez (antonioya) # # ---------------------------------------------------------- # noinspection PyUnresolvedReferences import bpy from math import radians, sin, cos from bpy.types import Operator from bpy.props import FloatProperty, BoolProperty, IntProperty, EnumProperty from .achm_tools import * # ------------------------------------------------------------------ # Define UI class # Stairs # ------------------------------------------------------------------ class ARCHIMESH_OT_Stairs(Operator): bl_idname = "mesh.archimesh_stairs" bl_label = "Stairs" bl_description = "Stairs Generator" bl_category = 'View' bl_options = {'REGISTER', 'UNDO'} # Define properties model: EnumProperty( items=( ('1', "Rectangular", ""), ('2', "Rounded", ""), ), name="Model", description="Type of steps", ) radio: FloatProperty( name='', min=0.001, max=0.500, default=0.20, precision=3, description='Radius factor for rounded', ) curve: BoolProperty( name="Include deformation handles", description="Include a curve to modify the stairs curve", default=False, ) step_num: IntProperty( name='Number of steps', min=1, max=1000, default=3, description='Number total of steps', ) max_width: FloatProperty( name='Width', min=0.001, max=10, default=1, precision=3, description='Step maximum width', ) depth: FloatProperty( name='Depth', min=0.001, max=10, default=0.30, precision=3, description='Depth of the step', ) shift: FloatProperty( name='Shift', min=0.001, max=1, default=1, precision=3, description='Step shift in Y axis', ) thickness: FloatProperty( name='Thickness', min=0.001, max=10, default=0.03, precision=3, description='Step thickness', ) sizev: BoolProperty( name="Variable width", description="Steps are not equal in width", default=False, ) back: BoolProperty( name="Close sides", description="Close all steps side to make a solid structure", default=False, ) min_width: FloatProperty( name='', min=0.001, max=10, default=1, precision=3, description='Step minimum width', ) height: FloatProperty( name='height', min=0.001, max=10, default=0.14, precision=3, description='Step height', ) front_gap: FloatProperty( name='Front', min=0, max=10, default=0.03, precision=3, description='Front gap', ) side_gap: FloatProperty( name='Side', min=0, max=10, default=0, precision=3, description='Side gap', ) crt_mat: BoolProperty( name="Create default Cycles materials", description="Create default materials for Cycles render", default=True, ) # ----------------------------------------------------- # Draw (create UI interface) # ----------------------------------------------------- # noinspection PyUnusedLocal def draw(self, context): layout = self.layout space = bpy.context.space_data if not space.local_view: # Imperial units warning if bpy.context.scene.unit_settings.system == "IMPERIAL": row = layout.row() row.label(text="Warning: Imperial units not supported", icon='COLOR_RED') box = layout.box() row = box.row() row.prop(self, 'model') if self.model == "2": row.prop(self, 'radio') box.prop(self, 'step_num') row = box.row() row.prop(self, 'max_width') row.prop(self, 'depth') row.prop(self, 'shift') row = box.row() row.prop(self, 'back') row.prop(self, 'sizev') row = box.row() row.prop(self, 'curve') # all equal if self.sizev is True: row.prop(self, 'min_width') box = layout.box() row = box.row() row.prop(self, 'thickness') row.prop(self, 'height') row = box.row() row.prop(self, 'front_gap') if self.model == "1": row.prop(self, 'side_gap') box = layout.box() if not context.scene.render.engine in {'CYCLES', 'BLENDER_EEVEE'}: box.enabled = False box.prop(self, 'crt_mat') else: row = layout.row() row.label(text="Warning: Operator does not work in local view mode", icon='ERROR') # ----------------------------------------------------- # Execute # ----------------------------------------------------- # noinspection PyUnusedLocal def execute(self, context): if bpy.context.mode == "OBJECT": create_stairs_mesh(self) return {'FINISHED'} else: self.report({'WARNING'}, "Archimesh: Option only valid in Object mode") return {'CANCELLED'} # ------------------------------------------------------------------------------ # Generate mesh data # All custom values are passed using self container (self.myvariable) # ------------------------------------------------------------------------------ def create_stairs_mesh(self): # deactivate others for o in bpy.data.objects: if o.select_get() is True: o.select_set(False) bpy.ops.object.select_all(action='DESELECT') # ------------------------ # Create stairs # ------------------------ mydata = create_stairs(self, "Stairs") mystairs = mydata[0] mystairs.select_set(True) bpy.context.view_layer.objects.active = mystairs remove_doubles(mystairs) set_normals(mystairs) set_modifier_mirror(mystairs, "X") # ------------------------ # Create curve handles # ------------------------ if self.curve: x = mystairs.location.x y = mystairs.location.y z = mystairs.location.z last = mydata[1] x1 = last[1] # use y myp = [((0, 0, 0), (- 0.25, 0, 0), (0.25, 0, 0)), ((x1, 0, 0), (x1 - 0.25, 0, 0), (x1 + 0.25, 0, 0))] # double element mycurve = create_bezier("Stairs_handle", myp, (x, y, z)) set_modifier_curve(mystairs, mycurve) # ------------------------ # Create materials # ------------------------ if self.crt_mat and bpy.context.scene.render.engine in {'CYCLES', 'BLENDER_EEVEE'}: # Stairs material mat = create_diffuse_material("Stairs_material", False, 0.8, 0.8, 0.8) set_material(mystairs, mat) bpy.ops.object.select_all(action='DESELECT') mystairs.select_set(True) bpy.context.view_layer.objects.active = mystairs return # ------------------------------------------------------------------------------ # Create rectangular Stairs # ------------------------------------------------------------------------------ def create_stairs(self, objname): myvertex = [] myfaces = [] index = 0 lastpoint = (0, 0, 0) for s in range(0, self.step_num): if self.model == "1": mydata = create_rect_step(self, lastpoint, myvertex, myfaces, index, s) if self.model == "2": mydata = create_round_step(self, lastpoint, myvertex, myfaces, index, s) index = mydata[0] lastpoint = mydata[1] mesh = bpy.data.meshes.new(objname) myobject = bpy.data.objects.new(objname, mesh) myobject.location = bpy.context.scene.cursor.location bpy.context.collection.objects.link(myobject) mesh.from_pydata(myvertex, [], myfaces) mesh.update(calc_edges=True) return myobject, lastpoint # ------------------------------------------------------------------------------ # Create rectangular step # ------------------------------------------------------------------------------ def create_rect_step(self, origin, myvertex, myfaces, index, step): x = origin[0] y = origin[1] z = origin[2] i = index max_depth = y + self.depth if self.back is True: max_depth = self.depth * self.step_num # calculate width (no side gap) if self.sizev is False: width = self.max_width / 2 else: width = (self.max_width / 2) - (step * (((self.max_width - self.min_width) / 2) / self.step_num)) # Vertical Rectangle myvertex.extend([(x, y, z), (x, y, z + self.height), (x + width, y, z + self.height), (x + width, y, z)]) val = y + self.thickness myvertex.extend([(x, val, z), (x, val, z + self.height), (x + width, val, z + self.height), (x + width, val, z)]) myfaces.extend([(i + 0, i + 1, i + 2, i + 3), (i + 4, i + 5, i + 6, i + 7), (i + 0, i + 3, i + 7, i + 4), (i + 1, i + 2, i + 6, i + 5), (i + 0, i + 1, i + 5, i + 4), (i + 3, i + 2, i + 6, i + 7)]) # Side plane myvertex.extend([(x + width, max_depth, z + self.height), (x + width, max_depth, z)]) myfaces.extend([(i + 7, i + 6, i + 8, i + 9)]) i += 10 # calculate width (side gap) width = width + self.side_gap # Horizontal Rectangle z = z + self.height myvertex.extend([(x, y - self.front_gap, z), (x, max_depth, z), (x + width, max_depth, z), (x + width, y - self.front_gap, z)]) z = z + self.thickness myvertex.extend([(x, y - self.front_gap, z), (x, max_depth, z), (x + width, max_depth, z), (x + width, y - self.front_gap, z)]) myfaces.extend([(i + 0, i + 1, i + 2, i + 3), (i + 4, i + 5, i + 6, i + 7), (i + 0, i + 3, i + 7, i + 4), (i + 1, i + 2, i + 6, i + 5), (i + 3, i + 2, i + 6, i + 7)]) i += 8 # remap origin y = y + (self.depth * self.shift) return i, (x, y, z) # ------------------------------------------------------------------------------ # Create rounded step # ------------------------------------------------------------------------------ def create_round_step(self, origin, myvertex, myfaces, index, step): x = origin[0] y = origin[1] z = origin[2] pos_x = None i = index li = [radians(270), radians(288), radians(306), radians(324), radians(342), radians(0)] max_width = self.max_width max_depth = y + self.depth if self.back is True: max_depth = self.depth * self.step_num # Resize for width if self.sizev is True: max_width = max_width - (step * ((self.max_width - self.min_width) / self.step_num)) half = max_width / 2 # ------------------------------------ # Vertical # ------------------------------------ # calculate width width = half - (half * self.radio) myradio = half - width myvertex.extend([(x, y, z), (x, y, z + self.height)]) # Round for e in li: pos_x = (cos(e) * myradio) + x + width - myradio pos_y = (sin(e) * myradio) + y + myradio myvertex.extend([(pos_x, pos_y, z), (pos_x, pos_y, z + self.height)]) # back point myvertex.extend([(x + width, max_depth, z), (x + width, max_depth, z + self.height)]) myfaces.extend([(i, i + 1, i + 3, i + 2), (i + 2, i + 3, i + 5, i + 4), (i + 4, i + 5, i + 7, i + 6), (i + 6, i + 7, i + 9, i + 8), (i + 8, i + 9, i + 11, i + 10), (i + 10, i + 11, i + 13, i + 12), (i + 12, i + 13, i + 15, i + 14)]) i += 16 # ------------------------------------ # Horizontal # ------------------------------------ # calculate width gap width = half + self.front_gap - (half * self.radio) z = z + self.height # Vertical myvertex.extend([(x, y - self.front_gap, z), (x, y - self.front_gap, z + self.thickness)]) # Round for e in li: pos_x = (cos(e) * myradio) + x + width - myradio pos_y = (sin(e) * myradio) + y + myradio - self.front_gap myvertex.extend([(pos_x, pos_y, z), (pos_x, pos_y, z + self.thickness)]) # back points myvertex.extend([(pos_x, max_depth, z), (pos_x, max_depth, z + self.thickness), (x, max_depth, z), (x, max_depth, z + self.thickness)]) myfaces.extend([(i, i + 1, i + 3, i + 2), (i + 2, i + 3, i + 5, i + 4), (i + 4, i + 5, i + 7, i + 6), (i + 6, i + 7, i + 9, i + 8), (i + 8, i + 9, i + 11, i + 10), (i + 10, i + 11, i + 13, i + 12), (i + 12, i + 13, i + 15, i + 14), (i, i + 2, i + 4, i + 6, i + 8, i + 10, i + 12, i + 14, i + 16), (i + 1, i + 3, i + 5, i + 7, i + 9, i + 11, i + 13, i + 15, i + 17), (i + 14, i + 15, i + 17, i + 16)]) i += 18 z = z + self.thickness # remap origin y = y + (self.depth * self.shift) return i, (x, y, z) # ------------------------------------------------------------------------------ # Create bezier curve # ------------------------------------------------------------------------------ def create_bezier(objname, points, origin): curvedata = bpy.data.curves.new(name=objname, type='CURVE') curvedata.dimensions = '3D' myobject = bpy.data.objects.new(objname, curvedata) myobject.location = origin myobject.rotation_euler[2] = radians(90) bpy.context.collection.objects.link(myobject) polyline = curvedata.splines.new('BEZIER') polyline.bezier_points.add(len(points) - 1) for idx, (knot, h1, h2) in enumerate(points): point = polyline.bezier_points[idx] point.co = knot point.handle_left = h1 point.handle_right = h2 point.handle_left_type = 'FREE' point.handle_right_type = 'FREE' return myobject