421 lines
14 KiB
Python
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
|