diff --git a/src/blenderbim/blenderbim/bim/module/model/prop.py b/src/blenderbim/blenderbim/bim/module/model/prop.py index 97a6c53bb8..2049020ce2 100644 --- a/src/blenderbim/blenderbim/bim/module/model/prop.py +++ b/src/blenderbim/blenderbim/bim/module/model/prop.py @@ -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 diff --git a/src/blenderbim/blenderbim/bim/module/model/stair.py b/src/blenderbim/blenderbim/bim/module/model/stair.py index 1a3e700f35..ed21e820b5 100644 --- a/src/blenderbim/blenderbim/bim/module/model/stair.py +++ b/src/blenderbim/blenderbim/bim/module/model/stair.py @@ -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) diff --git a/src/blenderbim/blenderbim/bim/module/model/ui.py b/src/blenderbim/blenderbim/bim/module/model/ui.py index 7d97d82498..b56ceab0b3 100644 --- a/src/blenderbim/blenderbim/bim/module/model/ui.py +++ b/src/blenderbim/blenderbim/bim/module/model/ui.py @@ -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: diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index 62ef5be96e..89db72daf4 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -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 diff --git a/src/blenderbim/test/tool/test_model.py b/src/blenderbim/test/tool/test_model.py index 8bab751641..af57133737 100644 --- a/src/blenderbim/test/tool/test_model.py +++ b/src/blenderbim/test/tool/test_model.py @@ -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),