Customize tread width for the first and last treads #3419

Available on all types of stairs, shouldn't break stairs previously created with BBIM.
Example - https://imgur.com/a/2wSueTM
This commit is contained in:
Andrej730
2023-11-03 11:26:29 +05:00
parent 3317dbd4a1
commit 12a6bf88a9
4 changed files with 149 additions and 43 deletions
@@ -361,7 +361,7 @@ class SverchokData:
def get_prop_from_data(props, data, prop_name):
prop_value = data[prop_name]
prop_value = data.get(prop_name, tool.Blender.get_blender_prop_default_value(props, prop_name))
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
prop_readable_name = props.bl_rna.properties[prop_name].name
return prop_readable_name, prop_value
@@ -210,6 +210,14 @@ class BIMStairProperties(PropertyGroup):
top_slab_depth: bpy.props.FloatProperty(name="Top Slab Depth", default=0.25, soft_min=0, subtype="DISTANCE")
has_top_nib: bpy.props.BoolProperty(name="Has Top Nib", default=True)
stair_type: bpy.props.EnumProperty(name="Stair Type", items=stair_types, default="CONCRETE")
custom_first_last_tread_run: bpy.props.FloatVectorProperty(
name="Custom First / Last Treads Widths",
description='Specify custom first / last treads widths, different from the general "Tread Run". Leave 0 to disable.',
default=(0, 0),
min=0,
unit="LENGTH",
size=2,
)
def get_props_kwargs(self, convert_to_project_units=False, stair_type=None):
if not stair_type:
@@ -240,6 +248,9 @@ class BIMStairProperties(PropertyGroup):
elif stair_type == "GENERIC":
pass
# defined here to appear last in UI
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
if not convert_to_project_units:
return stair_kwargs
@@ -266,7 +266,13 @@ class BIM_PT_stair(bpy.types.Panel):
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
row = self.layout.row(align=True)
for prop_name in props.get_props_kwargs():
self.layout.prop(props, prop_name)
prop_value = getattr(props, prop_name)
if isinstance(prop_value, bpy.types.bpy_prop_array):
prop_readable_name = props.bl_rna.properties[prop_name].name
self.layout.label(text=f"{prop_readable_name}:")
self.layout.prop(props, prop_name, text="")
else:
self.layout.prop(props, prop_name)
regenerate_stair_mesh(context)
else:
calculated_params = StairData.data["calculated_params"]
@@ -275,8 +281,14 @@ class BIM_PT_stair(bpy.types.Panel):
row = self.layout.row(align=True)
for prop_name, prop_value in StairData.data["general_params"].items():
row = self.layout.row(align=True)
row.label(text=prop_name)
row.label(text=str(prop_value))
if isinstance(prop_value, bpy.types.bpy_prop_array):
row.label(text=f"{prop_name}:")
row = self.layout.row(align=True)
for prop_value_item in prop_value:
row.label(text=str(prop_value_item))
else:
row.label(text=prop_name)
row.label(text=str(prop_value))
# calculated properties
for prop_name, prop_value in calculated_params.items():
+122 -39
View File
@@ -29,6 +29,8 @@ import blenderbim.core.tool
import blenderbim.tool as tool
import blenderbim.core.geometry as geometry
from mathutils import Matrix, Vector
from copy import deepcopy
from functools import partial
from blenderbim.bim import import_ifc
from blenderbim.bim.module.geometry.helper import Helper
from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData, WindowData, DoorData
@@ -917,16 +919,32 @@ class Model(blenderbim.core.tool.Model):
number_of_treads = props.number_of_treads
height = props.height / si_conversion
tread_run = props.tread_run / si_conversion
first_tread_run = props.custom_first_last_tread_run[0] / si_conversion
last_tread_run = props.custom_first_last_tread_run[1] / si_conversion
else:
number_of_treads = pset_data["number_of_treads"]
height = pset_data["height"]
tread_run = pset_data["tread_run"]
# use .get to not break the old .ifc models
custom_first_last_tread_run = pset_data.get("custom_first_last_tread_run", (0, 0))
first_tread_run, last_tread_run = custom_first_last_tread_run
calculated_params = {}
number_of_rises = number_of_treads + 1
calculated_params["Number of Risers"] = number_of_rises
calculated_params["Tread Rise"] = round(height / number_of_rises, 5)
calculated_params["Length"] = round(tread_run * number_of_rises, 5)
n_default_tread_runs = number_of_rises
length = 0
if first_tread_run != 0:
n_default_tread_runs -= 1
length += first_tread_run
if last_tread_run != 0:
n_default_tread_runs -= 1
if n_default_tread_runs >= 0:
length += last_tread_run
length += tread_run * max(n_default_tread_runs, 0)
calculated_params["Length"] = round(length, 5)
return calculated_params
@classmethod
@@ -943,6 +961,7 @@ class Model(blenderbim.core.tool.Model):
has_top_nib=None,
top_slab_depth=None,
base_slab_depth=None,
custom_first_last_tread_run=(0, 0),
):
"""returns a tuple of stair profile data: (vertices, edges, faces)"""
vertices = []
@@ -951,18 +970,35 @@ class Model(blenderbim.core.tool.Model):
number_of_risers = number_of_treads + 1
tread_rise = height / number_of_risers
length = tread_run * number_of_risers
custom_tread_run = any(run != 0 for run in custom_first_last_tread_run)
if stair_type == "WOOD/STEEL":
builder = ShapeBuilder(None)
tread_shape = builder.get_rectangle_coords(
size=V(tread_run, tread_depth), position=V(0, -(tread_depth - tread_rise))
)
tread_offset = V(tread_run, tread_rise)
# full tread rectangle
get_tread_verts = partial(builder.get_rectangle_coords, position=V(0, -(tread_depth - tread_rise)))
default_tread_verts = get_tread_verts(size=V(tread_run, tread_depth))
default_tread_offset = V(tread_run, tread_rise)
def get_tread_data(i):
if custom_tread_run:
current_tread_run = None
if i == 0 and custom_first_last_tread_run[0] != 0:
current_tread_run = custom_first_last_tread_run[0]
elif i == number_of_risers - 1 and custom_first_last_tread_run[1] != 0:
current_tread_run = custom_first_last_tread_run[1]
if current_tread_run:
tread_offset = default_tread_offset.copy()
tread_offset.x = current_tread_run
tread_verts = get_tread_verts(size=V(current_tread_run, tread_depth))
return tread_offset, tread_verts
return default_tread_offset, default_tread_verts
# each tread is a separate shape
cur_offset = V(0, 0)
for i in range(number_of_risers):
cur_trade_shape = [v + tread_offset * i for v in tread_shape]
tread_offset, tread_verts = get_tread_data(i)
cur_trade_shape = [v + cur_offset for v in tread_verts]
vertices.extend(cur_trade_shape)
cur_vertex = i * 4
@@ -973,19 +1009,40 @@ class Model(blenderbim.core.tool.Model):
(cur_vertex + 3, cur_vertex),
)
edges.extend(verts_to_add)
cur_offset += tread_offset
elif stair_type == "GENERIC":
vertices.append(Vector([0, 0]))
tread_verts = [Vector([0, tread_rise]), Vector([tread_run, tread_rise])]
tread_offset = Vector([tread_run, tread_rise])
# horizontal tread line
default_tread_verts = [Vector([0, tread_rise]), Vector([tread_run, tread_rise])]
default_tread_offset = Vector([tread_run, tread_rise])
def get_tread_data(i):
if custom_tread_run:
current_tread_run = None
if i == 0:
current_tread_run = custom_first_last_tread_run[0]
elif i == number_of_risers - 1:
current_tread_run = custom_first_last_tread_run[1]
if current_tread_run:
tread_offset = default_tread_offset.copy()
tread_offset.x = current_tread_run
tread_verts = deepcopy(default_tread_verts)
tread_verts[1].x = current_tread_run
return tread_offset, tread_verts
return default_tread_offset, default_tread_verts
# treads
current_offset = V(0, 0)
for i in range(number_of_risers):
current_tread_verts = [v + tread_offset * i for v in tread_verts]
tread_offset, tread_verts = get_tread_data(i)
current_tread_verts = [v + current_offset for v in tread_verts]
last_vert_i = i * 2
edges.extend([(last_vert_i, last_vert_i + 1), (last_vert_i + 1, last_vert_i + 2)])
vertices.extend(current_tread_verts)
current_offset += tread_offset
# close the shape
last_vert_i = len(vertices)
@@ -993,33 +1050,62 @@ class Model(blenderbim.core.tool.Model):
edges.extend([(last_vert_i - 1, last_vert_i), (last_vert_i, 0)])
elif stair_type == "CONCRETE":
# treads
for i in range(number_of_risers):
vertices.append(Vector((tread_run * i, tread_rise * i)))
vertices.append(Vector((tread_run * i, tread_rise * (i + 1))))
cur_vertex = i * 2
if i != 0:
edges.append((cur_vertex - 1, cur_vertex))
edges.append((cur_vertex, cur_vertex + 1))
vertices.append(V(0, 0))
vertices.append(Vector((tread_run * number_of_risers, tread_rise * number_of_risers)))
edges.append((number_of_risers * 2, number_of_risers * 2 - 1))
# NOTE: code until adding nibs is very similar to GENERIC
# horizontal tread line
default_tread_verts = [Vector([0, tread_rise]), Vector([tread_run, tread_rise])]
default_tread_offset = Vector([tread_run, tread_rise])
def get_tread_data(i):
if custom_tread_run:
current_tread_run = None
if i == 0:
current_tread_run = custom_first_last_tread_run[0]
elif i == number_of_risers - 1:
current_tread_run = custom_first_last_tread_run[1]
if current_tread_run:
tread_offset = default_tread_offset.copy()
tread_offset.x = current_tread_run
tread_verts = deepcopy(default_tread_verts)
tread_verts[1].x = current_tread_run
return tread_offset, tread_verts
return default_tread_offset, default_tread_verts
# treads
current_offset = V(0, 0)
for i in range(number_of_risers):
tread_offset, tread_verts = get_tread_data(i)
current_tread_verts = [v + current_offset for v in tread_verts]
last_vert_i = i * 2
edges.extend([(last_vert_i, last_vert_i + 1), (last_vert_i + 1, last_vert_i + 2)])
vertices.extend(current_tread_verts)
current_offset += tread_offset
# add the nibs
tread_diagonal_dir = vertices[2].normalized()
# basically we define stair bottom line as a line at `tread_depth` distance
# from the tread diagonal line
# we're going it define that line, sample it and abrupt it in case it meets a slab
# graph: https://www.desmos.com/calculator/bilmnti3cp
tread_diagonal_dir = V(tread_run, tread_rise).normalized()
# td_vector is clockwise orthogonal vector
td_vector = tread_diagonal_dir.yx * V(1, -1) * tread_depth
# graph: https://www.desmos.com/calculator/bilmnti3cp
stair_tan = tread_rise / tread_run
s0 = vertices[0] + td_vector
# s0 is just a sampled point from the bottom line
# we stick to the third point as the first point
# is affected by customized tread run
s0 = vertices[2] + td_vector
# comes from y = stair_tan * x + b
b = s0.y - stair_tan * s0.x
# if we use td_vector as depth_vector
# then stair won't be perpendicular to the X+
# to make it perpendicular to X+ we calculate `b`
# `b` is basically Z-offset where stair starts
depth_vector = V(0, b)
def get_point_on_2d_line(x=None, y=None):
if y is None:
y = stair_tan * x + b
elif x is None:
x = (y - b) / stair_tan
return V(x, y)
# top nib
last_vert = vertices[-1]
@@ -1027,30 +1113,27 @@ class Model(blenderbim.core.tool.Model):
# NOTE: has_top_nib = False and top_slab_depth are different things
if has_top_nib:
vertices.append(last_vert + Vector((0, -top_slab_depth)))
slab_overlaps_stair = abs(b) - top_slab_depth
# run distance by X until stair will meet the slab
run_slab_distance = slab_overlaps_stair / stair_tan
vertices.append(last_vert + Vector((run_slab_distance, -top_slab_depth)))
vertices.append(get_point_on_2d_line(y=last_vert.y - top_slab_depth))
edges.append((last_vertex_i, last_vertex_i + 1))
edges.append((last_vertex_i + 1, last_vertex_i + 2))
else:
vertices.append(last_vert + depth_vector)
new_vert = get_point_on_2d_line(last_vert.x)
vertices.append(new_vert)
edges.append((last_vertex_i, last_vertex_i + 1))
top_nib_end = len(vertices) - 1
# bottom nib
start_vert = vertices[0]
slab_overlaps_stair = max(abs(b) - base_slab_depth, 0)
if slab_overlaps_stair == 0:
base_point = get_point_on_2d_line(x=start_vert.x)
if base_point.y > -base_slab_depth:
# stair doesn't meet the slab
vertices.append(start_vert + depth_vector)
vertices.append(base_point)
edges.append((0, len(vertices) - 1))
bottom_nib_end = len(vertices) - 1
else:
# run distance by X until stair will meet the slab
run_slab_distance = slab_overlaps_stair / stair_tan
vertices.append(start_vert + Vector((run_slab_distance, -base_slab_depth)))
# slab overlaps stair
vertices.append(get_point_on_2d_line(y=start_vert.y - base_slab_depth))
vertices.append(start_vert + Vector((0, -base_slab_depth)))
last_vertex_i = len(vertices) - 1
edges.append((0, last_vertex_i))