diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index e5c0991e8e..55bdec760b 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -282,24 +282,37 @@ class ExtendProfile(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): selected_objs = context.selected_objects joiner = DumbProfileJoiner() + + # Handle unjoin if self.join_type == "-": for obj in selected_objs: joiner.unjoin(obj) return {"FINISHED"} + if not context.active_object: return {"FINISHED"} + for obj in selected_objs: tool.Geometry.clear_scale(obj) + # NEW: Extend all selected objects to cursor + if self.join_type == "E": + self._extend_to_cursor(selected_objs, context.scene.cursor.location) + return {"FINISHED"} + + # Single object - extend to cursor if len(selected_objs) == 1: joiner.join_E(context.active_object, context.scene.cursor.location) return {"FINISHED"} + # Two objects - L or V joints if len(selected_objs) == 2: if self.join_type == "L": joiner.join_L(next(o for o in selected_objs if o != context.active_object), context.active_object) elif self.join_type == "V": joiner.join_V(next(o for o in selected_objs if o != context.active_object), context.active_object) + + # Multiple objects - T joints if len(selected_objs) < 2: return {"FINISHED"} if self.join_type == "T": @@ -309,6 +322,59 @@ class ExtendProfile(bpy.types.Operator, tool.Ifc.Operator): joiner.join_T(obj, context.active_object) return {"FINISHED"} + + def _extend_to_cursor(self, objects: list[bpy.types.Object], cursor_location: Vector) -> None: + """Extend profiles or basic extrusions to cursor location.""" + joiner = DumbProfileJoiner() + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + + for obj in objects: + element = tool.Ifc.get_entity(obj) + if not element: + continue + + usage = tool.Model.get_usage_type(element) + + if usage == "PROFILE": + # Use existing profile logic + joiner.join_E(obj, cursor_location) + else: + # Handle basic extrusions + representation = tool.Geometry.get_active_representation(obj) + extrusion = tool.Model.get_extrusion(representation) if representation else None + + if not extrusion: + continue + + # Get extrusion data + if extrusion.Position: + position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist()) + position.translation *= unit_scale + else: + position = Matrix() + + # Get extrusion direction in world space + direction = Vector(extrusion.ExtrudedDirection.DirectionRatios).normalized() + extrusion_start = obj.matrix_world @ position.translation + extrusion_direction_world = (obj.matrix_world.to_quaternion() @ position.to_quaternion() @ direction).normalized() + + # Project cursor onto extrusion axis + cursor_vector = cursor_location - extrusion_start + projection_length = cursor_vector.dot(extrusion_direction_world) + + if projection_length > 0: + new_depth = projection_length / unit_scale + + # Update extrusion depth directly in IFC + extrusion.Depth = new_depth + + # Regenerate geometry + bonsai.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=representation, + ) class DumbProfileJoiner: diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index e44dabd916..834f6ab556 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -1453,44 +1453,59 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): cursor_z = bpy.context.scene.cursor.location.z layer2_objects = [] layer2_bases = [] + other_objects = [] for obj in bpy.context.selected_objects: element = tool.Ifc.get_entity(obj) - if element and tool.Model.get_usage_type(element) == "LAYER2": + if not element: + continue + + usage = tool.Model.get_usage_type(element) + if usage == "LAYER2": obj_base_z = obj.matrix_world.translation.z layer2_objects.append(obj) layer2_bases.append(obj_base_z) + else: + other_objects.append(obj) - if not layer2_objects: - self.report({"ERROR"}, "No LAYER2 objects selected") - return + # Handle LAYER2 objects - extend height to cursor Z + if layer2_objects: + tolerance = 1e-5 + if layer2_bases and (max(layer2_bases) - min(layer2_bases)) > tolerance: + min_base = min(layer2_bases) + max_base = max(layer2_bases) + self.report( + {"ERROR"}, + f"Selected LAYER2 objects have different base heights ({min_base:.3f}m to {max_base:.3f}m). " + f"All objects must be at the exact same base level (tolerance {tolerance}).", + ) + return - # --- tolerance check --- - tolerance = 1e-5 # to provide a little wiggle room - if layer2_bases and (max(layer2_bases) - min(layer2_bases)) > tolerance: - min_base = min(layer2_bases) - max_base = max(layer2_bases) - self.report( - {"ERROR"}, - f"Selected LAYER2 objects have different base heights ({min_base:.3f}m to {max_base:.3f}m). " - f"All objects must be at the exact same base level (tolerance {tolerance}).", - ) - return + common_base = sum(layer2_bases) / len(layer2_bases) + new_height = cursor_z - common_base - # use the mean base as the "common" one to avoid floating-point mismatches - common_base = sum(layer2_bases) / len(layer2_bases) - new_height = cursor_z - common_base + if new_height > 0: + props = tool.Model.get_model_props() + props.extrusion_depth = new_height + bpy.ops.bim.change_extrusion_depth(depth=new_height) + self.report({"INFO"}, f"Extended {len(layer2_objects)} LAYER2 object(s) to z: {cursor_z:.2f}m") + else: + self.report( + {"ERROR"}, + f"Negative height not allowed. Cursor ({cursor_z:.2f}m) must be above object base ({common_base:.2f}m)", + ) - if new_height > 0: - props = tool.Model.get_model_props() - props.extrusion_depth = new_height - bpy.ops.bim.change_extrusion_depth(depth=new_height) - self.report({"INFO"}, f"Extended {len(layer2_objects)} LAYER2 object(s) to z: {cursor_z:.2f}m") - else: - self.report( - {"ERROR"}, - f"Negative height not allowed. Cursor ({cursor_z:.2f}m) must be above object base ({common_base:.2f}m)", - ) + # Handle PROFILE objects and basic extrusions - extend to 3D cursor + if other_objects: + # Temporarily deselect layer2 objects + for obj in layer2_objects: + obj.select_set(False) + + bpy.ops.bim.extend_profile(join_type="E") + + # Restore selection + for obj in layer2_objects: + obj.select_set(True) custom_icon_previews = None