mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-28 15:53:00 +00:00
closes #3565 - extend profiles and basic extrusions to 3D cursor with Ctrl+E
Previously Ctrl+E only worked for LAYER2 elements (walls) to extend height to cursor Z position. This adds support for: - PROFILE elements (beams, columns, members) - extends to cursor in 3D - Basic extrusions - extends along extrusion direction to cursor - Multiple objects simultaneously using join_type="E"
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user