From 036ee098a627dbd14d5eda0883c90e7207be8fec Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sat, 21 Feb 2026 18:24:41 -0600 Subject: [PATCH] Fix slab layer geometry: custom offset isolation, unit scale, and layer ordering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three related bugs fixed in the slab material layer set workflow: 1. EditAssignedMaterial (operator.py): Applying a custom offset to one slab instance incorrectly regenerated geometry for ALL slabs sharing the same IfcMaterialLayerSet. Replaced regenerate_from_layer_set (sweeps all users) with per-element regenerate_from_occurence for AXIS3 slabs and targeted recalculate_walls for AXIS2 walls. Each element's IfcMaterialLayerSetUsage attributes are now updated individually before regeneration. 2. slice_layerset_mesh (loader.py): Loader.unit_scale is a class variable only set during full file import, so it was stale (= 1) during live geometry updates on foot-based IFC files. Layer bisect planes were being computed in IFC feet while the mesh was in Blender metres, placing all cuts completely outside the mesh. Fixed by computing unit_scale fresh from the IFC file on each call via ifcopenshell.util.unit.calculate_unit_scale. 3. OffsetFromReferenceLine stale / layer order reversed (slab.py, loader.py): A guard (and custom_offset is None) in change_thickness prevented writing the correct OffsetFromReferenceLine (position.z) to the usage when a custom offset was active. This left the value at 0.0 instead of the actual slab bottom (e.g. -1.0 IFC units for a TOP-reference slab), so the bisect starting point co was at the reference plane rather than the slab bottom, reversing layer assignments or missing layers entirely. Removed the guard — safe because each element has its own IfcMaterialLayerSetUsage instance. Reverted the AXIS3 bisect normal back to (0,0,1) (upward from co at slab bottom) which is correct once OffsetFromReferenceLine is properly set. --- .../bonsai/bim/module/material/operator.py | 18 +++++++++++------- src/bonsai/bonsai/bim/module/model/slab.py | 3 ++- src/bonsai/bonsai/tool/loader.py | 15 +++++++++++---- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index e0761bf416..7d8f9edd9d 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -614,25 +614,29 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator): attributes=attributes, ) - layer_sets_to_regenerate = set() + slab_planer = slab.DumbSlabPlaner() + wall_objs = [] for obj in objects: obj_element = tool.Ifc.get_entity(obj) obj_material_usage = ifcopenshell.util.element.get_material(obj_element) - if obj_material_usage and obj_material_usage.is_a("IfcMaterialLayerSetUsage"): obj_material_usage.OffsetFromReferenceLine = material.OffsetFromReferenceLine obj_material_usage.DirectionSense = material.DirectionSense obj_material_usage.ReferenceExtent = material.ReferenceExtent - layer_sets_to_regenerate.add(obj_material_usage.ForLayerSet) - # Save custom offset to BBIM_MaterialLayer pset tool.Model.save_custom_offset_to_pset(obj_element, obj) - for layer_set in layer_sets_to_regenerate: - wall.DumbWallPlaner().regenerate_from_layer_set(layer_set) - slab.DumbSlabPlaner().regenerate_from_layer_set(layer_set) + # Targeted regeneration: only update this element's geometry, not + # all elements sharing the layer set (which would corrupt unrelated instances). + if obj_material_usage.LayerSetDirection == "AXIS3": + slab_planer.regenerate_from_occurence(obj_element, obj_material_usage) + elif obj_material_usage.LayerSetDirection == "AXIS2": + wall_objs.append(obj) + + if wall_objs: + tool.Model.recalculate_walls(wall_objs) if material_set_usage.is_a("IfcMaterialProfileSetUsage"): if "CardinalPoint" in attributes: diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index 58a353ab28..a2957048a9 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -280,6 +280,8 @@ class DumbSlabPlaner: perpendicular_depth = thickness * abs(1 / cos(existing_x_angle)) perpendicular_offset = layer_offset * abs(1 / cos(existing_x_angle)) / self.unit_scale + ifc_position = extrusion.Position + # Check angle and z direction to determine whether the extrusion direction is positive or negative if (abs(existing_x_angle) < (pi / 2) and direction_ratios.z > 0) or ( abs(existing_x_angle) > (pi / 2) and direction_ratios.z < 0 @@ -301,7 +303,6 @@ class DumbSlabPlaner: extrusion.ExtrudedDirection.DirectionRatios = tuple(direction_ratios) extrusion.Depth = perpendicular_depth - ifc_position = extrusion.Position position = offset_direction * perpendicular_offset material = ifcopenshell.util.element.get_material(element) if material: diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index 6d30192485..8d11f79670 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1063,12 +1063,16 @@ class Loader(bonsai.core.tool.Loader): @classmethod def slice_layerset_mesh(cls, element: ifcopenshell.entity_instance, mesh: bpy.types.Mesh) -> bpy.types.Mesh: + # Always compute unit_scale fresh — cls.unit_scale may be stale (e.g. during live + # geometry updates that don't go through the full import pipeline). + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + if not (material := ifcopenshell.util.element.get_material(element)): return mesh elif material.is_a("IfcMaterialLayerSetUsage"): usage = material layer_set = material.ForLayerSet - offset = usage.OffsetFromReferenceLine * cls.unit_scale + offset = usage.OffsetFromReferenceLine * unit_scale sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1 else: return mesh @@ -1101,7 +1105,7 @@ class Loader(bonsai.core.tool.Loader): for i, layer in enumerate(layer_set.MaterialLayers): if i != last_i: prev_co = co.copy() - co += no * layer.LayerThickness * cls.unit_scale + co += no * layer.LayerThickness * unit_scale bisect_geom = bmesh.ops.bisect_plane( bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no ) @@ -1115,14 +1119,17 @@ class Loader(bonsai.core.tool.Loader): for face in bisect_geom["geom"]: if isinstance(face, bmesh.types.BMFace): center = face.calc_center_median() - if (center - co).dot(no) >= 0: + dot = (center - co).dot(no) + if dot >= 0: face.material_index = material_index has_layer_styles = True else: for face in bisect_geom["geom"]: if isinstance(face, bmesh.types.BMFace): center = face.calc_center_median() - if (center - co).dot(no) < 0 and (center - prev_co).dot(no) >= 0: + dot_co = (center - co).dot(no) + dot_prev = (center - prev_co).dot(no) + if dot_co < 0 and dot_prev >= 0: face.material_index = material_index has_layer_styles = True