From d79524b087fb7e4099584f4cbebe3e8e8eb46457 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Thu, 15 Jan 2026 14:21:39 -0600 Subject: [PATCH] fix #7559: DirectionSense works again. --- src/bonsai/bonsai/bim/module/model/slab.py | 88 +++++----------- src/bonsai/bonsai/tool/loader.py | 111 +++++++++++++++++---- 2 files changed, 117 insertions(+), 82 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index 2b2c3a2984..f5765331b6 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -225,16 +225,17 @@ class DumbSlabPlaner: for inverse in tool.Ifc.get().get_inverse(layer_set): if not inverse.is_a("IfcMaterialLayerSetUsage") or inverse.LayerSetDirection != "AXIS3": continue + if tool.Ifc.get().schema == "IFC2X3": for rel in tool.Ifc.get().get_inverse(inverse): if not rel.is_a("IfcRelAssociatesMaterial"): continue for element in rel.RelatedObjects: - self.change_thickness(element, total_thickness) + self.change_thickness(element, total_thickness, preserve_offset=True) else: for rel in inverse.AssociatedTo: for element in rel.RelatedObjects: - self.change_thickness(element, total_thickness) + self.change_thickness(element, total_thickness, preserve_offset=True) def regenerate_from_type(self, usecase_path, ifc_file, settings): relating_type = settings["relating_type"] @@ -276,9 +277,10 @@ class DumbSlabPlaner: return self.change_thickness(element, total_thickness) - def change_thickness(self, element: ifcopenshell.entity_instance, thickness: float) -> None: + def change_thickness(self, element: ifcopenshell.entity_instance, thickness: float, preserve_offset: bool = False) -> None: if tool.Model.get_usage_type(element) != "LAYER3": return + layer_params = tool.Model.get_material_layer_parameters(element) ifc_file = tool.Ifc.get() body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") @@ -297,86 +299,47 @@ class DumbSlabPlaner: extrusion = tool.Model.get_extrusion(representation) if extrusion: direction_ratios = Vector(extrusion.ExtrudedDirection.DirectionRatios) - + # Calculate the actual extrusion angle from vertical extrusion_angle = 0 if direction_ratios.length > 0: cos_angle = direction_ratios.normalized().dot(Vector((0, 0, 1))) extrusion_angle = acos(min(max(cos_angle, -1), 1)) - # FIX: Only apply 1/cos factor when there's actual extrusion slope + # Only apply 1/cos factor when there's actual extrusion slope if extrusion_angle > 1e-6: perpendicular_depth = thickness * abs(1 / cos(extrusion_angle)) - perpendicular_offset = layer_offset * abs(1 / cos(extrusion_angle)) / self.unit_scale + perpendicular_offset = layer_offset * abs(1 / cos(extrusion_angle)) else: perpendicular_depth = thickness - perpendicular_offset = layer_offset / self.unit_scale + perpendicular_offset = layer_offset - # Check if direction sense needs to be applied - # This should only happen if explicitly requested, not automatically - if layer_params.get("apply_direction_sense", False): - # Store current direction before potential change - old_direction = direction_ratios.copy() - - # Apply direction sense logic - existing_x_angle = extrusion_angle - if (abs(existing_x_angle) < (pi / 2) and direction_ratios.z > 0) or ( - abs(existing_x_angle) > (pi / 2) and direction_ratios.z < 0 - ): - if layer_params["direction_sense"] == "NEGATIVE": - direction_ratios *= -1 - elif (abs(existing_x_angle) > (pi / 2) and direction_ratios.z > 0) or ( - abs(existing_x_angle) < (pi / 2) and direction_ratios.z < 0 - ): - offset_direction = direction_ratios.copy() * -1 - if layer_params["direction_sense"] == "POSITIVE": - direction_ratios *= -1 - - # If direction changed, update extrusion with rotation compensation - if (direction_ratios.normalized() - old_direction.normalized()).length > 1e-6: - update_extrusion_direction(element, tuple(direction_ratios), obj) - # After updating direction, get the updated extrusion - extrusion = tool.Model.get_extrusion(representation) - - # Update depth extrusion.Depth = perpendicular_depth # Update position ifc_position = extrusion.Position + if direction_ratios.length > 0: offset_vector = direction_ratios.normalized() * perpendicular_offset position = offset_vector material = ifcopenshell.util.element.get_material(element) if material and material.is_a("IfcMaterialLayerSetUsage"): - material.OffsetFromReferenceLine = position.z + # Only set offset if not preserving it (preserves independent offsets per instance) + if not preserve_offset: + material.OffsetFromReferenceLine = position.z if ifc_position: ifc_position.Location.Coordinates = position else: tool.Model.add_extrusion_position(extrusion, position) - else: - props = tool.Model.get_model_props() - x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else props.x_angle - new_rep = ifcopenshell.api.geometry.add_slab_representation( - tool.Ifc.get(), - context=body_context, - depth=thickness * self.unit_scale, - x_angle=x_angle, - ) - for inverse in tool.Ifc.get().get_inverse(representation): - ifcopenshell.util.element.replace_attribute(inverse, representation, new_rep) - bonsai.core.geometry.switch_representation( - tool.Ifc, - tool.Geometry, - obj=obj, - representation=new_rep, - ) - bonsai.core.geometry.remove_representation( - tool.Ifc, tool.Geometry, obj=obj, representation=representation - ) - return + bonsai.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=representation, + ) else: props = tool.Model.get_model_props() x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else props.x_angle @@ -389,13 +352,12 @@ class DumbSlabPlaner: ifcopenshell.api.geometry.assign_representation( tool.Ifc.get(), product=element, representation=representation ) - - bonsai.core.geometry.switch_representation( - tool.Ifc, - tool.Geometry, - obj=obj, - representation=representation, - ) + bonsai.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=representation, + ) def update_extrusion_direction( element: ifcopenshell.entity_instance, new_direction_ratios: tuple, obj: bpy.types.Object = None diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index a15a3f8a29..529d06a27b 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1021,7 +1021,7 @@ class Loader(bonsai.core.tool.Loader): elif material.is_a("IfcMaterialLayerSetUsage"): usage = material layer_set = material.ForLayerSet - offset = usage.OffsetFromReferenceLine * cls.unit_scale + offset = usage.OffsetFromReferenceLine sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1 elif material.is_a("IfcMaterialLayerSet"): usage = None @@ -1034,11 +1034,17 @@ class Loader(bonsai.core.tool.Loader): if len(layer_set.MaterialLayers) == 1: return mesh + # Get mesh bounds + if len(mesh.vertices) > 0: + z_coords = [v.co.z for v in mesh.vertices] + mesh_z_min = min(z_coords) + mesh_z_max = max(z_coords) + bm = bmesh.new() bm.from_mesh(mesh) prev_co = None - advance_direction = None # Will store direction to advance planes + advance_direction = None if not usage: sense_factor = 1 @@ -1046,9 +1052,7 @@ class Loader(bonsai.core.tool.Loader): co = Vector((0.0, 0.0, offset)) advance_direction = no elif usage.LayerSetDirection == "AXIS2": - co = Vector((0.0, offset, 0.0)) - - # Get LOCAL extrusion direction + # Get local extrusion direction local_extrusion = Vector([0.0, 0.0, 1.0]) if body := ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW"): for item in ifcopenshell.util.representation.resolve_representation(body).Items: @@ -1060,17 +1064,58 @@ class Loader(bonsai.core.tool.Loader): # Thickness direction: perpendicular to extrusion and length thickness_dir = local_extrusion.cross(Vector([1.0, 0.0, 0.0])).normalized() - - # Ensure it points in POSITIVE Y (through wall thickness, not backwards) if thickness_dir.y < 0: thickness_dir = -thickness_dir no = thickness_dir + + # Find start point by projecting vertices onto thickness direction + if len(mesh.vertices) > 0: + projections = [Vector(v.co).dot(no) for v in mesh.vertices] + min_proj = min(projections) + max_proj = max(projections) + + centroid = sum((Vector(v.co) for v in mesh.vertices), Vector()) / len(mesh.vertices) + centroid_proj = centroid.dot(no) + + if sense_factor == 1: + start_proj = min_proj + else: + start_proj = max_proj + + offset_dist = start_proj - centroid_proj + co = centroid + no * offset_dist + + actual_mesh_height = max_proj - min_proj + else: + co = Vector((0.0, 0.0, 0.0)) + advance_direction = thickness_dir elif usage.LayerSetDirection == "AXIS3": - co = Vector((0.0, 0.0, offset)) - no = cls.get_extrusion_vector(element).normalized() + # AXIS3 layers go through slab thickness (local Z) no = Vector([0.0, 0.0, 1.0]) + + # Find start point by projecting vertices onto Z direction + if len(mesh.vertices) > 0: + projections = [Vector(v.co).dot(no) for v in mesh.vertices] + min_proj = min(projections) + max_proj = max(projections) + + centroid = sum((Vector(v.co) for v in mesh.vertices), Vector()) / len(mesh.vertices) + centroid_proj = centroid.dot(no) + + if sense_factor == 1: + start_proj = min_proj + else: + start_proj = max_proj + + offset = start_proj - centroid_proj + co = centroid + no * offset + + actual_mesh_height = max_proj - min_proj + else: + co = Vector((0.0, 0.0, 0.0)) + advance_direction = no elif usage.LayerSetDirection == "AXIS1": co = Vector((0.0, 0.0, offset)) @@ -1078,10 +1123,27 @@ class Loader(bonsai.core.tool.Loader): no = Vector([1.0, 0.0, 0.0]) advance_direction = no - no *= sense_factor - advance_direction *= sense_factor + # Apply DirectionSense + if usage and usage.LayerSetDirection == "AXIS2": + if sense_factor == -1: + advance_direction = -advance_direction + test_normal = -no + else: + test_normal = no + elif usage and usage.LayerSetDirection == "AXIS1": + no = no * sense_factor + advance_direction = advance_direction * sense_factor + test_normal = no + elif usage and usage.LayerSetDirection == "AXIS3": + if sense_factor == -1: + advance_direction = -advance_direction + test_normal = -no + else: + test_normal = no + else: + test_normal = no - # Cache this + # Cache material styles body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") styles = {} has_layer_styles = False @@ -1089,12 +1151,23 @@ class Loader(bonsai.core.tool.Loader): if style := tool.Ifc.get_entity(material): styles[style] = i + layer_list = list(enumerate(layer_set.MaterialLayers)) + + # Calculate scale factor + total_layer_thickness = sum(layer.LayerThickness for _, layer in layer_list) + + if 'actual_mesh_height' not in locals(): + actual_mesh_height = mesh_z_max - mesh_z_min if len(mesh.vertices) > 0 else total_layer_thickness + + thickness_scale = actual_mesh_height / total_layer_thickness if total_layer_thickness > 0 else 1.0 + last_i = len(layer_set.MaterialLayers) - 1 - for i, layer in enumerate(layer_set.MaterialLayers): - if i != last_i: + + for idx, (original_i, layer) in enumerate(layer_list): + if idx != last_i: prev_co = co.copy() - # Use advance_direction (not no) to move planes! - co += advance_direction * layer.LayerThickness * cls.unit_scale + advance_vector = advance_direction * layer.LayerThickness * thickness_scale + co += advance_vector bisect_geom = bmesh.ops.bisect_plane( bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no @@ -1107,18 +1180,18 @@ class Loader(bonsai.core.tool.Loader): material_index = len(mesh.materials) mesh.materials.append(tool.Ifc.get_object(style)) - if i == last_i: + if idx == last_i: for face in bisect_geom["geom"]: if isinstance(face, bmesh.types.BMFace): center = face.calc_center_median() - if (center - co).dot(no) >= 0: + if (center - co).dot(test_normal) >= 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: + if (center - co).dot(test_normal) < 0 and (center - prev_co).dot(test_normal) >= 0: face.material_index = material_index has_layer_styles = True