mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
Nosing Lengths / Tread Gap for parametric stairs #3422
Example - https://imgur.com/d8PxLV4
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user