Skip to content

Commit f52aafd

Browse files
committed
closes #7249: for stairs, change the first/last tread lengths to less than the typical tread run. And even go to zero, whereby removing the tread altogether.
1 parent b0ae0a2 commit f52aafd

5 files changed

Lines changed: 364 additions & 46 deletions

File tree

src/bonsai/bonsai/bim/module/model/prop.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,12 @@ def validate_nosing_value(self, context: bpy.types.Context) -> None:
373373
if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0:
374374
self["nosing_length"] = 0
375375

376-
non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type")
376+
def update_custom_tread_lock(self, context: bpy.types.Context) -> None:
377+
"""When lock is enabled, sync custom treads with tread_run"""
378+
if self.custom_tread_lock:
379+
self["custom_first_last_tread_run"] = (self.tread_run, self.tread_run)
380+
381+
non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type", "custom_tread_lock")
377382

378383
is_editing: bpy.props.BoolProperty(default=False)
379384
width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01, subtype="DISTANCE")
@@ -407,6 +412,12 @@ def validate_nosing_value(self, context: bpy.types.Context) -> None:
407412
default="CONCRETE",
408413
update=validate_nosing_value,
409414
)
415+
custom_tread_lock: bpy.props.BoolProperty(
416+
name="Lock First/Last Treads to Tread Run",
417+
description="When enabled, first and last treads automatically use the Tread Run value",
418+
default=True,
419+
update=update_custom_tread_lock,
420+
)
410421
custom_first_last_tread_run: bpy.props.FloatVectorProperty(
411422
name="Custom First / Last Treads Widths",
412423
description='Specify custom first / last treads widths, different from the general "Tread Run". Leave 0 to disable.',
@@ -442,6 +453,7 @@ def validate_nosing_value(self, context: bpy.types.Context) -> None:
442453
top_slab_depth: float
443454
has_top_nib: bool
444455
stair_type: str
456+
custom_tread_lock: bool
445457
custom_first_last_tread_run: tuple[float, float]
446458
nosing_length: float
447459
nosing_depth: float
@@ -480,17 +492,38 @@ def get_props_kwargs(self, convert_to_project_units=False, stair_type=None):
480492
}
481493
stair_kwargs.update(generic_props)
482494

483-
# defined here to appear last in UI
484-
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
495+
# If locked, use tread_run for both first and last treads
496+
if self.custom_tread_lock:
497+
stair_kwargs["custom_first_last_tread_run"] = (self.tread_run, self.tread_run)
498+
else:
499+
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
485500

486501
if not convert_to_project_units:
487502
return stair_kwargs
488503

489504
stair_kwargs = tool.Model.convert_data_to_project_units(stair_kwargs, self.non_si_units_props)
490505
return stair_kwargs
506+
507+
def get_props_kwargs_for_ifc_export(self, convert_to_project_units=False, stair_type=None):
508+
"""Get props including custom_tread_lock for saving to IFC"""
509+
stair_kwargs = self.get_props_kwargs(convert_to_project_units, stair_type)
510+
# Add the lock state for IFC storage (after getting base kwargs to avoid passing to generate function)
511+
stair_kwargs["custom_tread_lock"] = self.custom_tread_lock
512+
return stair_kwargs
491513

492514
def set_props_kwargs_from_ifc_data(self, kwargs):
493515
kwargs = tool.Model.convert_data_to_si_units(kwargs, self.non_si_units_props)
516+
517+
# Determine lock state based on whether custom treads match tread_run
518+
# If custom_tread_lock wasn't saved (old files), infer it from the data
519+
if "custom_tread_lock" not in kwargs:
520+
custom_treads = kwargs.get("custom_first_last_tread_run", (0.0, 0.0))
521+
tread_run = kwargs.get("tread_run", 0.3)
522+
# Lock is off if either custom tread differs from tread_run and is not 0
523+
kwargs["custom_tread_lock"] = not any(
524+
ct != 0.0 and ct != tread_run for ct in custom_treads
525+
)
526+
494527
for prop_name in kwargs:
495528
setattr(self, prop_name, kwargs[prop_name])
496529

src/bonsai/bonsai/bim/module/model/stair.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,8 @@ def _execute(self, context):
188188
props = tool.Model.get_stair_props(obj)
189189
ifc_file = tool.Ifc.get()
190190

191-
stair_data = props.get_props_kwargs(convert_to_project_units=True)
191+
# Use the special method that includes custom_tread_lock for IFC storage
192+
stair_data = props.get_props_kwargs_for_ifc_export(convert_to_project_units=True)
192193
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
193194
if not pset:
194195
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="BBIM_Stair")
@@ -241,7 +242,8 @@ def _execute(self, context):
241242
assert element
242243
props = tool.Model.get_stair_props(obj)
243244

244-
data = props.get_props_kwargs(convert_to_project_units=True)
245+
# Use the special method that includes custom_tread_lock for IFC storage
246+
data = props.get_props_kwargs_for_ifc_export(convert_to_project_units=True)
245247
props.is_editing = False
246248
regenerate_stair_mesh(obj)
247249
tool.Model.add_body_representation(obj)

src/bonsai/bonsai/bim/module/model/ui.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,13 +302,36 @@ def draw(self, context):
302302
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
303303
row = self.layout.row(align=True)
304304
for prop_name in props.get_props_kwargs():
305+
# Skip custom_tread_lock as it's handled with custom_first_last_tread_run
306+
if prop_name == "custom_tread_lock":
307+
continue
308+
305309
prop_value = getattr(props, prop_name)
306-
if isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
310+
311+
# Special handling for custom_first_last_tread_run
312+
if prop_name == "custom_first_last_tread_run":
313+
# Draw the lock toggle
314+
row_lock = self.layout.row(align=True)
315+
lock_text = "Lock First/Last Treads" if not props.custom_tread_lock else "Unlock First/Last Treads"
316+
row_lock.prop(
317+
props,
318+
"custom_tread_lock",
319+
text=lock_text,
320+
icon="LOCKED" if props.custom_tread_lock else "UNLOCKED",
321+
)
322+
323+
# Only show the custom values input if unlocked
324+
if not props.custom_tread_lock:
325+
prop_readable_name = props.bl_rna.properties[prop_name].name
326+
self.layout.label(text=f"{prop_readable_name}:")
327+
self.layout.prop(props, prop_name, text="")
328+
elif isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
307329
prop_readable_name = props.bl_rna.properties[prop_name].name
308330
self.layout.label(text=f"{prop_readable_name}:")
309331
self.layout.prop(props, prop_name, text="")
310332
else:
311333
self.layout.prop(props, prop_name)
334+
312335
if prop_name == "height": # Weak but we just want to insert this inside props drawing
313336
row_length = self.layout.row(align=True)
314337
row_length.prop(props, "total_length_target")

src/bonsai/bonsai/tool/model.py

Lines changed: 62 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,7 +1399,6 @@ def generate_stair_2d_profile(
13991399

14001400
number_of_risers = number_of_treads + 1
14011401
tread_rise = height / number_of_risers
1402-
custom_tread_run = any(run != 0 for run in custom_first_last_tread_run)
14031402
nosing_overlap = max(nosing_length, 0)
14041403
nosing_tread_gap = -min(nosing_length, 0)
14051404
nosing_overlap_offset = -V_(nosing_overlap, 0)
@@ -1430,29 +1429,40 @@ def define_generic_stair_treads():
14301429
default_tread_offset = Vector([tread_run, tread_rise])
14311430

14321431
def get_tread_data(i):
1433-
if custom_tread_run:
1434-
current_tread_run = None
1435-
if i == 0:
1436-
current_tread_run = custom_first_last_tread_run[0]
1437-
elif i == number_of_risers - 1:
1438-
current_tread_run = custom_first_last_tread_run[1]
1439-
1440-
if current_tread_run:
1441-
tread_offset = default_tread_offset.copy()
1442-
tread_offset.x = current_tread_run
1443-
tread_verts = deepcopy(default_tread_verts)
1444-
tread_verts[-1].x = current_tread_run
1445-
return tread_offset, tread_verts
1432+
# Check if this is first or last tread with custom run
1433+
current_tread_run = None
1434+
if i == 0 and custom_first_last_tread_run[0] is not None:
1435+
current_tread_run = custom_first_last_tread_run[0]
1436+
elif i == number_of_risers - 1 and custom_first_last_tread_run[1] is not None:
1437+
current_tread_run = custom_first_last_tread_run[1]
1438+
1439+
if current_tread_run is not None:
1440+
tread_offset = default_tread_offset.copy()
1441+
tread_offset.x = current_tread_run
1442+
1443+
# Handle zero-width treads
1444+
if current_tread_run == 0:
1445+
# For zero width, just return vertical offset with no horizontal tread
1446+
return tread_offset, ()
1447+
1448+
tread_verts = deepcopy(default_tread_verts)
1449+
tread_verts[-1].x = current_tread_run
1450+
return tread_offset, tread_verts
1451+
14461452
return default_tread_offset, default_tread_verts
14471453

14481454
# treads
14491455
current_offset = V_(0, 0)
14501456
for i in range(number_of_risers):
14511457
last_vert_i = len(vertices) - 1
14521458
tread_offset, tread_verts = get_tread_data(i)
1453-
current_tread_verts = [v + current_offset for v in tread_verts]
1454-
edges.extend(default_tread_edges + last_vert_i)
1455-
vertices.extend(current_tread_verts)
1459+
1460+
# Skip adding vertices/edges for zero-width treads
1461+
if tread_verts:
1462+
current_tread_verts = [v + current_offset for v in tread_verts]
1463+
edges.extend(default_tread_edges + last_vert_i)
1464+
vertices.extend(current_tread_verts)
1465+
14561466
current_offset += tread_offset
14571467

14581468
if stair_type == "WOOD/STEEL":
@@ -1467,35 +1477,47 @@ def get_tread_verts(*args, **kwargs):
14671477
default_tread_offset = V_(tread_run + nosing_tread_gap, tread_rise)
14681478

14691479
def get_tread_data(i):
1470-
if custom_tread_run:
1471-
current_tread_run = None
1472-
if i == 0 and custom_first_last_tread_run[0] != 0:
1473-
current_tread_run = custom_first_last_tread_run[0]
1474-
elif i == number_of_risers - 1 and custom_first_last_tread_run[1] != 0:
1475-
current_tread_run = custom_first_last_tread_run[1]
1476-
1477-
if current_tread_run:
1478-
tread_offset = default_tread_offset.copy()
1479-
tread_offset.x = current_tread_run + nosing_tread_gap
1480-
tread_verts = get_tread_verts(size=V_(current_tread_run + nosing_overlap, tread_depth))
1481-
return tread_offset, tread_verts
1480+
# Check if this is first or last tread with custom run
1481+
current_tread_run = None
1482+
if i == 0 and custom_first_last_tread_run[0] is not None:
1483+
current_tread_run = custom_first_last_tread_run[0]
1484+
elif i == number_of_risers - 1 and custom_first_last_tread_run[1] is not None:
1485+
current_tread_run = custom_first_last_tread_run[1]
1486+
1487+
if current_tread_run is not None:
1488+
tread_offset = default_tread_offset.copy()
1489+
tread_offset.x = current_tread_run + nosing_tread_gap
1490+
1491+
# Handle zero-width treads
1492+
if current_tread_run == 0:
1493+
return tread_offset, ()
1494+
1495+
tread_verts = get_tread_verts(size=V_(current_tread_run + nosing_overlap, tread_depth))
1496+
return tread_offset, tread_verts
1497+
14821498
return default_tread_offset, default_tread_verts
14831499

14841500
# each tread is a separate shape
14851501
cur_offset = V_(0, 0)
1502+
tread_index = 0
14861503
for i in range(number_of_risers):
14871504
tread_offset, tread_verts = get_tread_data(i)
1488-
cur_trade_shape = [v + cur_offset + nosing_overlap_offset for v in tread_verts]
1489-
vertices.extend(cur_trade_shape)
1490-
1491-
cur_vertex = i * 4
1492-
verts_to_add = (
1493-
(cur_vertex, cur_vertex + 1),
1494-
(cur_vertex + 1, cur_vertex + 2),
1495-
(cur_vertex + 2, cur_vertex + 3),
1496-
(cur_vertex + 3, cur_vertex),
1497-
)
1498-
edges.extend(verts_to_add)
1505+
1506+
# Skip adding vertices/edges for zero-width treads
1507+
if tread_verts:
1508+
cur_trade_shape = [v + cur_offset + nosing_overlap_offset for v in tread_verts]
1509+
vertices.extend(cur_trade_shape)
1510+
1511+
cur_vertex = tread_index * 4
1512+
verts_to_add = (
1513+
(cur_vertex, cur_vertex + 1),
1514+
(cur_vertex + 1, cur_vertex + 2),
1515+
(cur_vertex + 2, cur_vertex + 3),
1516+
(cur_vertex + 3, cur_vertex),
1517+
)
1518+
edges.extend(verts_to_add)
1519+
tread_index += 1
1520+
14991521
cur_offset += tread_offset
15001522

15011523
elif stair_type == "GENERIC":

0 commit comments

Comments
 (0)