Nosing Lengths / Tread Gap for parametric stairs #3422

Example - https://imgur.com/d8PxLV4
This commit is contained in:
Andrej730
2023-11-06 11:50:52 +05:00
parent a218ce5160
commit d9fdc0908a
5 changed files with 112 additions and 76 deletions
@@ -193,6 +193,10 @@ class BIMArrayProperties(PropertyGroup):
class BIMStairProperties(PropertyGroup):
def validate_nosing_value(self, context):
if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0:
self["nosing_length"] = 0
non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type")
stair_types = (
("CONCRETE", "Concrete", ""),
@@ -209,7 +213,9 @@ class BIMStairProperties(PropertyGroup):
base_slab_depth: bpy.props.FloatProperty(name="Base Slab Depth", default=0.25, soft_min=0, subtype="DISTANCE")
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")
stair_type: bpy.props.EnumProperty(
name="Stair Type", items=stair_types, default="CONCRETE", update=validate_nosing_value
)
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.',
@@ -218,6 +224,20 @@ class BIMStairProperties(PropertyGroup):
unit="LENGTH",
size=2,
)
# TODO: need to clamp at zero for non WOOD/STEEL
nosing_length: bpy.props.FloatProperty(
name="Nosing Length",
description=(
"Overhang of the tread, not counted as a part of the tread run.\n"
"Can be negative for WOOD/STEEL stair (then it becomes a tread gap)"
),
default=0,
unit="LENGTH",
update=validate_nosing_value,
)
nosing_depth: bpy.props.FloatProperty(
name="Nosing Depth", description="Depth of the tread's nosing", min=0, default=0, unit="LENGTH"
)
def get_props_kwargs(self, convert_to_project_units=False, stair_type=None):
if not stair_type:
@@ -228,10 +248,12 @@ class BIMStairProperties(PropertyGroup):
"height": self.height,
"number_of_treads": self.number_of_treads,
"tread_run": self.tread_run,
"nosing_length": self.nosing_length,
}
if stair_type == "CONCRETE":
concrete_props = {
"nosing_depth": self.nosing_depth,
"base_slab_depth": self.base_slab_depth,
"top_slab_depth": self.top_slab_depth,
"has_top_nib": self.has_top_nib,
@@ -246,7 +268,10 @@ class BIMStairProperties(PropertyGroup):
stair_kwargs.update(wood_steel_props)
elif stair_type == "GENERIC":
pass
generic_props = {
"nosing_depth": self.nosing_depth,
}
stair_kwargs.update(generic_props)
# defined here to appear last in UI
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
@@ -146,6 +146,7 @@ def update_ifc_stair_props(obj):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
riser_height = props.height / number_of_risers / si_conversion
tread_length = props.tread_depth / si_conversion
nosing_length = props.nosing_length / si_conversion
if element.is_a("IfcStairFlight"):
if tool.Ifc.get_schema() == "IFC2X3":
@@ -171,6 +172,7 @@ def update_ifc_stair_props(obj):
"NumberOfTreads": props.number_of_treads,
"RiserHeight": riser_height,
"TreadLength": tread_length,
"NosingLength": nosing_length,
},
)
tool.Ifc.edit(obj)
@@ -36,6 +36,7 @@ from blenderbim.bim.module.model.door import update_door_modifier_bmesh
from blenderbim.bim.module.model.railing import update_railing_modifier_bmesh
from blenderbim.bim.module.model.roof import update_roof_modifier_bmesh
from blenderbim.bim.helper import prop_with_search
from collections.abc import Iterable
class LaunchTypeManager(bpy.types.Operator):
@@ -267,7 +268,7 @@ class BIM_PT_stair(bpy.types.Panel):
row = self.layout.row(align=True)
for prop_name in props.get_props_kwargs():
prop_value = getattr(props, prop_name)
if isinstance(prop_value, bpy.types.bpy_prop_array):
if isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
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="")
@@ -281,7 +282,7 @@ 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)
if isinstance(prop_value, bpy.types.bpy_prop_array):
if isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
row.label(text=f"{prop_name}:")
row = self.layout.row(align=True)
for prop_value_item in prop_value:
+77 -69
View File
@@ -921,6 +921,7 @@ class Model(blenderbim.core.tool.Model):
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
nosing_length = props.nosing_length / si_conversion
else:
number_of_treads = pset_data["number_of_treads"]
height = pset_data["height"]
@@ -928,12 +929,14 @@ class Model(blenderbim.core.tool.Model):
# 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
nosing_length = pset_data.get("nosing_length", 0)
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)
# calculate stair length
n_default_tread_runs = number_of_rises
length = 0
if first_tread_run != 0:
@@ -944,7 +947,16 @@ class Model(blenderbim.core.tool.Model):
if n_default_tread_runs >= 0:
length += last_tread_run
length += tread_run * max(n_default_tread_runs, 0)
# nosing overlaps
# are not part of the tread run
# so they don't affect the stair length
# except the first tread's nosing
if nosing_length > 0: # nosing overlaps
length += nosing_length
if nosing_length < 0: # tread gaps
length += abs(nosing_length) * number_of_treads
calculated_params["Length"] = round(length, 5)
return calculated_params
@classmethod
@@ -962,6 +974,9 @@ class Model(blenderbim.core.tool.Model):
top_slab_depth=None,
base_slab_depth=None,
custom_first_last_tread_run=(0, 0),
nosing_length=0,
# CONCRETE GENERIC STAIR ARGUMENTS
nosing_depth=0,
):
"""returns a tuple of stair profile data: (vertices, edges, faces)"""
vertices = []
@@ -971,13 +986,67 @@ class Model(blenderbim.core.tool.Model):
number_of_risers = number_of_treads + 1
tread_rise = height / number_of_risers
custom_tread_run = any(run != 0 for run in custom_first_last_tread_run)
nosing_overlap = max(nosing_length, 0)
nosing_tread_gap = -min(nosing_length, 0)
nosing_overlap_offset = -V(nosing_overlap, 0)
def define_generic_stair_treads():
vertices.append(Vector([0, 0]))
nonlocal nosing_depth, nosing_overlap
# avoid weird geometry
nosing_depth = min(nosing_depth, tread_rise)
nosing_overlap = min(nosing_overlap, tread_run)
default_tread_edges = np.array(((0, 1), (1, 2)))
# horizontal tread line
if nosing_overlap == 0:
default_tread_verts = (V(0, tread_rise), V(tread_run, tread_rise))
elif nosing_depth == 0:
default_tread_verts = (V(-nosing_overlap, tread_rise), V(tread_run, tread_rise))
else: # nosing_overlap > 0 nosing_depth > 0
# kind of L shape
default_tread_verts = (
V(0, tread_rise - nosing_depth),
V(-nosing_overlap, tread_rise - nosing_depth),
V(-nosing_overlap, tread_rise),
V(tread_run, tread_rise),
)
add_edges = ((2, 3), (3, 4))
default_tread_edges = np.concatenate((default_tread_edges, add_edges))
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):
last_vert_i = len(vertices) - 1
tread_offset, tread_verts = get_tread_data(i)
current_tread_verts = [v + current_offset for v in tread_verts]
edges.extend(default_tread_edges + last_vert_i)
vertices.extend(current_tread_verts)
current_offset += tread_offset
if stair_type == "WOOD/STEEL":
builder = ShapeBuilder(None)
# 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)
default_tread_verts = get_tread_verts(size=V(tread_run + nosing_overlap, tread_depth))
default_tread_offset = V(tread_run + nosing_tread_gap, tread_rise)
def get_tread_data(i):
if custom_tread_run:
@@ -989,8 +1058,8 @@ class Model(blenderbim.core.tool.Model):
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))
tread_offset.x = current_tread_run + nosing_tread_gap
tread_verts = get_tread_verts(size=V(current_tread_run + nosing_overlap, tread_depth))
return tread_offset, tread_verts
return default_tread_offset, default_tread_verts
@@ -998,7 +1067,7 @@ class Model(blenderbim.core.tool.Model):
cur_offset = V(0, 0)
for i in range(number_of_risers):
tread_offset, tread_verts = get_tread_data(i)
cur_trade_shape = [v + cur_offset for v in tread_verts]
cur_trade_shape = [v + cur_offset + nosing_overlap_offset for v in tread_verts]
vertices.extend(cur_trade_shape)
cur_vertex = i * 4
@@ -1012,37 +1081,7 @@ class Model(blenderbim.core.tool.Model):
cur_offset += tread_offset
elif stair_type == "GENERIC":
vertices.append(Vector([0, 0]))
# 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
define_generic_stair_treads()
# close the shape
last_vert_i = len(vertices)
@@ -1050,38 +1089,7 @@ class Model(blenderbim.core.tool.Model):
edges.extend([(last_vert_i - 1, last_vert_i), (last_vert_i, 0)])
elif stair_type == "CONCRETE":
vertices.append(V(0, 0))
# 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
define_generic_stair_treads()
# add the nibs
# basically we define stair bottom line as a line at `tread_depth` distance
@@ -1096,7 +1104,7 @@ class Model(blenderbim.core.tool.Model):
# 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
s0 = V(custom_first_last_tread_run[0] or tread_run, tread_rise) + td_vector
# comes from y = stair_tan * x + b
b = s0.y - stair_tan * s0.x
+3 -3
View File
@@ -113,7 +113,7 @@ class TestGenerateStair2DProfile(NewFile):
verts_gen, edges_gen, faces_gen = generated_profile
verts, edges, faces = expected_profile
assert edges == tuple(edges_gen)
assert np.all(edges == np.array(edges_gen))
assert faces == tuple(tuple(face) for face in faces_gen)
for vert, vert_gen in zip(verts, verts_gen, strict=True):
assert tool.Cad.are_vectors_equal(vert, vert_gen, 0.01)
@@ -152,7 +152,7 @@ class TestGenerateStair2DProfile(NewFile):
(4, 5),
(5, 6),
(6, 7),
(8, 7),
(7, 8),
(8, 9),
(0, 11),
(10, 11),
@@ -199,7 +199,7 @@ class TestGenerateStair2DProfile(NewFile):
(4, 5),
(5, 6),
(6, 7),
(8, 7),
(7, 8),
(8, 9),
(9, 10),
(0, 12),