expre-gen/blender/3.4/scripts/addons/archimesh/achm_stairs_maker.py
2025-10-17 09:47:23 +08:00

421 lines
14 KiB
Python

# 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