From b46136e9f8f77f0d8eca106ea7170896a602d626 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Wed, 5 Nov 2025 06:46:18 -0600 Subject: [PATCH] For stairs, Lock total length now takes first and last tread widths into account as well. --- src/bonsai/bonsai/bim/module/model/prop.py | 141 +++++++++++++++++++-- src/bonsai/bonsai/tool/model.py | 86 +++++++++++-- 2 files changed, 204 insertions(+), 23 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index a286e3daee..88c7218697 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -345,26 +345,138 @@ class BIMArrayProperties(PropertyGroup): method: Literal["OFFSET", "DISTRIBUTE"] sync_children: bool relating_array_object: Union[bpy.types.Object, None] - - def update_total_length_target(self: "BIMStairProperties", context: bpy.types.Context) -> None: - self["tread_run"] = self.total_length_target / (self.number_of_treads + 1) + """Update tread_run when total_length_target changes""" + # Calculate available length for default treads + available_length = self.total_length_target + n_default_treads = self.number_of_treads + 1 # number of risers + + # Subtract custom first tread if not locked and not zero + if not self.custom_tread_lock and self.custom_first_last_tread_run[0] != 0: + available_length -= self.custom_first_last_tread_run[0] + n_default_treads -= 1 + + # Subtract custom last tread if not locked and not zero + if not self.custom_tread_lock and self.custom_first_last_tread_run[1] != 0: + available_length -= self.custom_first_last_tread_run[1] + n_default_treads -= 1 + + # Calculate tread_run for remaining treads + if n_default_treads > 0: + self["tread_run"] = available_length / n_default_treads + else: + # All treads are custom, just use target length + self["tread_run"] = self.total_length_target / (self.number_of_treads + 1) def update_tread_run(self: "BIMStairProperties", context: bpy.types.Context) -> None: + """Update either number_of_treads or total_length_target when tread_run changes""" if self.total_length_lock: - self["number_of_treads"] = int((self.total_length_target / self.tread_run) - 1) + # Calculate how much length custom treads take up + custom_length = 0 + n_custom_treads = 0 + + if not self.custom_tread_lock: + if self.custom_first_last_tread_run[0] != 0: + custom_length += self.custom_first_last_tread_run[0] + n_custom_treads += 1 + if self.custom_first_last_tread_run[1] != 0: + custom_length += self.custom_first_last_tread_run[1] + n_custom_treads += 1 + + # Calculate how many default treads fit in remaining space + available_length = self.total_length_target - custom_length + if self.tread_run > 0: + n_default_treads = available_length / self.tread_run + total_treads = n_default_treads + n_custom_treads + # number_of_treads = number_of_risers - 1 + self["number_of_treads"] = int(total_treads - 1) else: - self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run + # Calculate total length from current settings + n_default_treads = self.number_of_treads + 1 + total_length = 0 + + if not self.custom_tread_lock: + if self.custom_first_last_tread_run[0] != 0: + total_length += self.custom_first_last_tread_run[0] + n_default_treads -= 1 + if self.custom_first_last_tread_run[1] != 0: + total_length += self.custom_first_last_tread_run[1] + n_default_treads -= 1 + + total_length += n_default_treads * self.tread_run + self["total_length_target"] = total_length def update_number_of_treads(self: "BIMStairProperties", context: bpy.types.Context) -> None: + """Update either tread_run or total_length_target when number_of_treads changes""" if self.total_length_lock: - self["tread_run"] = self.total_length_target / (self.number_of_treads + 1) + # Calculate available length for default treads + available_length = self.total_length_target + n_default_treads = self.number_of_treads + 1 + + if not self.custom_tread_lock: + if self.custom_first_last_tread_run[0] != 0: + available_length -= self.custom_first_last_tread_run[0] + n_default_treads -= 1 + if self.custom_first_last_tread_run[1] != 0: + available_length -= self.custom_first_last_tread_run[1] + n_default_treads -= 1 + + if n_default_treads > 0: + self["tread_run"] = available_length / n_default_treads + else: + self["tread_run"] = self.total_length_target / (self.number_of_treads + 1) else: - self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run + # Calculate total length from current settings + n_default_treads = self.number_of_treads + 1 + total_length = 0 + + if not self.custom_tread_lock: + if self.custom_first_last_tread_run[0] != 0: + total_length += self.custom_first_last_tread_run[0] + n_default_treads -= 1 + if self.custom_first_last_tread_run[1] != 0: + total_length += self.custom_first_last_tread_run[1] + n_default_treads -= 1 + + total_length += n_default_treads * self.tread_run + self["total_length_target"] = total_length +def update_custom_first_last_tread_run(self: "BIMStairProperties", context: bpy.types.Context) -> None: + """Update tread_run or total_length when custom treads change""" + if self.total_length_lock: + # Recalculate tread_run to maintain total length + available_length = self.total_length_target + n_default_treads = self.number_of_treads + 1 + + if not self.custom_tread_lock: + if self.custom_first_last_tread_run[0] != 0: + available_length -= self.custom_first_last_tread_run[0] + n_default_treads -= 1 + if self.custom_first_last_tread_run[1] != 0: + available_length -= self.custom_first_last_tread_run[1] + n_default_treads -= 1 + + if n_default_treads > 0: + self["tread_run"] = available_length / n_default_treads + else: + # Recalculate total length + n_default_treads = self.number_of_treads + 1 + total_length = 0 + + if not self.custom_tread_lock: + if self.custom_first_last_tread_run[0] != 0: + total_length += self.custom_first_last_tread_run[0] + n_default_treads -= 1 + if self.custom_first_last_tread_run[1] != 0: + total_length += self.custom_first_last_tread_run[1] + n_default_treads -= 1 + + total_length += n_default_treads * self.tread_run + self["total_length_target"] = total_length + StairType = Literal["CONCRETE", "WOOD/STEEL", "GENERIC"] @@ -419,13 +531,14 @@ class BIMStairProperties(PropertyGroup): update=update_custom_tread_lock, ) 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, - ) + 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, + update=update_custom_first_last_tread_run, # Added update callback + ) nosing_length: bpy.props.FloatProperty( name="Nosing Length", description=( diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index ea6cb176bb..0d54a500f8 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -1324,7 +1324,7 @@ class Model(bonsai.core.tool.Model): @classmethod def is_parametric_door_active(cls) -> bool: return bool((DoorData.is_loaded or not DoorData.load()) and DoorData.data["pset_data"]) - + @classmethod def get_active_stair_calculated_params(cls, pset_data: Optional[dict[str, Any]] = None) -> dict[str, Any]: props = bpy.context.active_object.BIMStairProperties @@ -1352,24 +1352,31 @@ class Model(bonsai.core.tool.Model): calculated_params["Tread Rise"] = round(height / number_of_rises, 5) # calculate stair length + # Start with all treads using default tread_run n_default_tread_runs = number_of_rises length = 0 + + # If first tread has custom width (non-zero), use it instead of default if first_tread_run != 0: n_default_tread_runs -= 1 length += first_tread_run + + # If last tread has custom width (non-zero), use it instead of default 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) - # nosing overlaps - # are not part of the tread run - # so they don't affect the stair length - # except the first tread's nosing + length += last_tread_run + + # Add remaining default tread runs + length += tread_run * n_default_tread_runs + + # Handle nosing length effects on total length + # Nosing overlaps don't affect tread run spacing, + # but the first tread's nosing extends the total length if nosing_length > 0: # nosing overlaps length += nosing_length - if nosing_length < 0: # tread gaps + if nosing_length < 0: # tread gaps between treads length += abs(nosing_length) * number_of_treads + calculated_params["Length"] = round(length, 5) pitch = height / length pitch_formatted = str(round(pitch * 100, 1)) + " % / " + str(round(degrees(atan(pitch)), 1)) + " deg" @@ -1407,6 +1414,28 @@ class Model(bonsai.core.tool.Model): nosing_tread_gap = -min(nosing_length, 0) nosing_overlap_offset = -V_(nosing_overlap, 0) + # ============ DEBUG LOGGING START ============ + print("\n" + "="*80) + print("STAIR GENERATION DEBUG - WOOD/STEEL TYPE") + print("="*80) + print(f"Input Parameters:") + print(f" stair_type: {stair_type}") + print(f" number_of_treads: {number_of_treads}") + print(f" height: {height}") + print(f" width: {width}") + print(f" tread_run: {tread_run}") + print(f" tread_depth: {tread_depth}") + print(f" custom_first_last_tread_run: {custom_first_last_tread_run}") + print(f" nosing_length: {nosing_length}") + print(f"\nCalculated Parameters:") + print(f" number_of_risers: {number_of_risers}") + print(f" tread_rise: {tread_rise}") + print(f" nosing_overlap: {nosing_overlap}") + print(f" nosing_tread_gap: {nosing_tread_gap}") + print(f" nosing_overlap_offset: {nosing_overlap_offset}") + print("="*80 + "\n") + # ============ DEBUG LOGGING END ============ + def define_generic_stair_treads(): vertices.append(Vector([0, 0])) nonlocal nosing_depth, nosing_overlap @@ -1470,6 +1499,10 @@ class Model(bonsai.core.tool.Model): current_offset += tread_offset if stair_type == "WOOD/STEEL": + print("\n" + "-"*80) + print("WOOD/STEEL STAIR GENERATION PROCESS") + print("-"*80) + builder = ShapeBuilder(None) # full tread rectangle @@ -1480,6 +1513,11 @@ class Model(bonsai.core.tool.Model): 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) + print(f"\nDefault Tread Configuration:") + print(f" default_tread_verts: {default_tread_verts}") + print(f" default_tread_offset: {default_tread_offset}") + print(f" tread rectangle size: ({tread_run + nosing_overlap}, {tread_depth})") + def get_tread_data(i): # Check if this is first or last tread with custom run current_tread_run = None @@ -1488,28 +1526,48 @@ class Model(bonsai.core.tool.Model): elif i == number_of_risers - 1 and custom_first_last_tread_run[1] is not None: current_tread_run = custom_first_last_tread_run[1] + print(f"\n Tread {i}:") + print(f" Is first tread: {i == 0}") + print(f" Is last tread: {i == number_of_risers - 1}") + print(f" current_tread_run: {current_tread_run}") + if current_tread_run is not None: tread_offset = default_tread_offset.copy() tread_offset.x = current_tread_run + nosing_tread_gap # Handle zero-width treads if current_tread_run == 0: + print(f" → Zero-width tread, skipping geometry") + print(f" → tread_offset: {tread_offset}") return tread_offset, () tread_verts = get_tread_verts(size=V_(current_tread_run + nosing_overlap, tread_depth)) + print(f" → Custom tread") + print(f" → tread_offset: {tread_offset}") + print(f" → tread_verts: {tread_verts}") + print(f" → tread rectangle size: ({current_tread_run + nosing_overlap}, {tread_depth})") return tread_offset, tread_verts + print(f" → Using default tread") + print(f" → tread_offset: {default_tread_offset}") return default_tread_offset, default_tread_verts # each tread is a separate shape cur_offset = V_(0, 0) tread_index = 0 + + print(f"\nGenerating Treads:") + print(f" Number of risers to process: {number_of_risers}") + for i in range(number_of_risers): tread_offset, tread_verts = get_tread_data(i) # Skip adding vertices/edges for zero-width treads if tread_verts: cur_trade_shape = [v + cur_offset + nosing_overlap_offset for v in tread_verts] + print(f" Adding geometry at cur_offset: {cur_offset}") + print(f" Final vertices (after offset): {cur_trade_shape}") + vertices.extend(cur_trade_shape) cur_vertex = tread_index * 4 @@ -1520,9 +1578,19 @@ class Model(bonsai.core.tool.Model): (cur_vertex + 3, cur_vertex), ) edges.extend(verts_to_add) + print(f" Edges added: {verts_to_add}") tread_index += 1 + else: + print(f" Skipped (no geometry)") cur_offset += tread_offset + print(f" New cur_offset: {cur_offset}") + + print(f"\nFinal Results:") + print(f" Total vertices: {len(vertices)}") + print(f" Total edges: {len(edges)}") + print(f" Total treads generated: {tread_index}") + print("-"*80 + "\n") elif stair_type == "GENERIC": define_generic_stair_treads()