mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-10-03 05:22:06 +00:00
fix #7537 - Layer thickness correct when slab is rotated and few other features...
- Add dual-rotation support for AXIS3 slabs (IFC angle + object rotation) - Fix profile editing to display horizontal projection for tilted slabs - Fix AXIS2 layer slicing to use local extrusion direction for walls - Fix ChangeExtrusionDepth to refresh geometry after depth changes - Remove rotation lock on slabs to allow free rotation - Fix undefined variable bug in add_slab_representation.py
This commit is contained in:
@@ -35,7 +35,7 @@ import bonsai.core.geometry
|
||||
import bonsai.core.root
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from math import cos, pi
|
||||
from math import cos, sin, pi, acos, degrees
|
||||
from mathutils import Vector, Matrix
|
||||
from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator
|
||||
from bonsai.bim.module.model.polyline import PolylineOperator
|
||||
@@ -296,50 +296,65 @@ class DumbSlabPlaner:
|
||||
if representation:
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
if extrusion:
|
||||
# TODO Right now we don't have a reliable way to calculate the existing x_angle only based solely on the extrusion direction.
|
||||
# For instances, a 30 degrees angled extrusion with positive direction has the same extrusion direction as a
|
||||
# -150 degrees angled extrusion with negative direction. The difference lies in the object's rotation.
|
||||
# This means that things can get messy if the user changes the object x angle somehow. We have to figure out an alternative approach.
|
||||
existing_x_angle = obj.rotation_euler.x
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 2 * pi, tolerance=0.001) else existing_x_angle
|
||||
direction_ratios = Vector(extrusion.ExtrudedDirection.DirectionRatios)
|
||||
offset_direction = direction_ratios.copy()
|
||||
perpendicular_depth = thickness * abs(1 / cos(existing_x_angle))
|
||||
perpendicular_offset = layer_offset * abs(1 / cos(existing_x_angle)) / self.unit_scale
|
||||
|
||||
# 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
|
||||
):
|
||||
# The extrusion direction is positive. If the layer_parameter is set to negative,
|
||||
# then the we change the extrusion direction.
|
||||
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
|
||||
):
|
||||
# The extrusion direction is negative. If the layer_parameter is set to positive,
|
||||
# then the we change the extrusion direction. And the offset direction should remain positive
|
||||
# for either direction sense, so we change it.
|
||||
offset_direction *= -1
|
||||
if layer_params["direction_sense"] == "POSITIVE":
|
||||
direction_ratios *= -1
|
||||
|
||||
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:
|
||||
if material.is_a("IfcMaterialLayerSetUsage"):
|
||||
material.OffsetFromReferenceLine = position.z
|
||||
if ifc_position:
|
||||
ifc_position.Location.Coordinates = position
|
||||
|
||||
# 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
|
||||
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
|
||||
else:
|
||||
tool.Model.add_extrusion_position(extrusion, position)
|
||||
perpendicular_depth = thickness
|
||||
perpendicular_offset = layer_offset / self.unit_scale
|
||||
|
||||
# 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
|
||||
|
||||
if ifc_position:
|
||||
ifc_position.Location.Coordinates = position
|
||||
else:
|
||||
tool.Model.add_extrusion_position(extrusion, position)
|
||||
|
||||
else:
|
||||
props = tool.Model.get_model_props()
|
||||
@@ -383,6 +398,113 @@ class DumbSlabPlaner:
|
||||
)
|
||||
|
||||
|
||||
def update_extrusion_direction(element: ifcopenshell.entity_instance,
|
||||
new_direction_ratios: tuple,
|
||||
obj: bpy.types.Object = None) -> None:
|
||||
"""
|
||||
Update extrusion direction while preserving overall object orientation.
|
||||
|
||||
Args:
|
||||
element: The IFC element
|
||||
new_direction_ratios: New extrusion direction ratios (x,y,z)
|
||||
obj: Optional Blender object (will be fetched if not provided)
|
||||
"""
|
||||
if not obj:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if not obj:
|
||||
return
|
||||
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if not representation:
|
||||
return
|
||||
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
if not extrusion:
|
||||
return
|
||||
|
||||
# Get current extrusion direction
|
||||
old_direction = Vector(extrusion.ExtrudedDirection.DirectionRatios)
|
||||
if old_direction.length == 0:
|
||||
old_direction = Vector((0, 0, 1)) # Default
|
||||
|
||||
new_direction = Vector(new_direction_ratios)
|
||||
if new_direction.length == 0:
|
||||
new_direction = Vector((0, 0, 1)) # Default
|
||||
|
||||
# Normalize both directions
|
||||
old_direction_normalized = old_direction.normalized()
|
||||
new_direction_normalized = new_direction.normalized()
|
||||
|
||||
# Store current object matrix
|
||||
old_matrix = obj.matrix_world.copy()
|
||||
|
||||
# Calculate the rotation needed to keep same orientation
|
||||
# When extrusion direction changes from A to B relative to local coordinates,
|
||||
# we need to rotate the object by the inverse of that change
|
||||
|
||||
# Calculate rotation from old to new direction
|
||||
rotation_axis = old_direction_normalized.cross(new_direction_normalized)
|
||||
if rotation_axis.length > 1e-6:
|
||||
rotation_axis.normalized()
|
||||
dot_product = old_direction_normalized.dot(new_direction_normalized)
|
||||
angle = acos(min(max(dot_product, -1), 1))
|
||||
|
||||
# Apply INVERSE rotation to object to compensate
|
||||
rotation_matrix = Matrix.Rotation(-angle, 4, rotation_axis)
|
||||
|
||||
# Update object rotation
|
||||
obj.matrix_world = old_matrix @ rotation_matrix
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
# Update extrusion direction (keeping magnitude)
|
||||
if old_direction.length > 0:
|
||||
# Preserve the magnitude of the original direction vector
|
||||
magnitude = old_direction.length
|
||||
new_direction = new_direction_normalized * magnitude
|
||||
|
||||
extrusion.ExtrudedDirection.DirectionRatios = tuple(new_direction)
|
||||
|
||||
# Update depth based on new extrusion angle
|
||||
extrusion_angle = 0
|
||||
if new_direction.length > 0:
|
||||
cos_angle = new_direction_normalized.dot(Vector((0, 0, 1)))
|
||||
extrusion_angle = acos(min(max(cos_angle, -1), 1))
|
||||
|
||||
# Get current depth (perpendicular depth)
|
||||
current_perpendicular_depth = extrusion.Depth
|
||||
|
||||
# If we have material layer info, calculate actual thickness
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
actual_thickness = current_perpendicular_depth
|
||||
if material and material.is_a("IfcMaterialLayerSetUsage"):
|
||||
layer_set = material.ForLayerSet
|
||||
actual_thickness = sum([l.LayerThickness for l in layer_set.MaterialLayers])
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
actual_thickness *= unit_scale
|
||||
|
||||
# Convert to perpendicular depth if needed
|
||||
if extrusion_angle > 1e-6:
|
||||
new_perpendicular_depth = actual_thickness * abs(1 / cos(extrusion_angle))
|
||||
else:
|
||||
new_perpendicular_depth = actual_thickness
|
||||
|
||||
extrusion.Depth = new_perpendicular_depth
|
||||
|
||||
# Update position offset if needed
|
||||
if extrusion.Position:
|
||||
# Recalculate offset based on new direction
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
if material and material.is_a("IfcMaterialLayerSetUsage"):
|
||||
offset = material.OffsetFromReferenceLine
|
||||
if extrusion_angle > 1e-6:
|
||||
perpendicular_offset = offset * abs(1 / cos(extrusion_angle))
|
||||
else:
|
||||
perpendicular_offset = offset
|
||||
|
||||
offset_vector = new_direction_normalized * perpendicular_offset
|
||||
extrusion.Position.Location.Coordinates = tuple(offset_vector)
|
||||
|
||||
|
||||
class EnableEditingSketchExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_sketch_extrusion_profile"
|
||||
bl_label = "Enable Editing Sketch Extrusion Profile"
|
||||
@@ -656,6 +778,8 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
extrusion = tool.Model.get_extrusion(body)
|
||||
existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
|
||||
layer_params = tool.Model.get_material_layer_parameters(element)
|
||||
|
||||
usage_type = tool.Model.get_usage_type(element)
|
||||
|
||||
if extrusion.Position:
|
||||
position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist())
|
||||
@@ -669,22 +793,49 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tranlation_matrix = Matrix.Translation(rot_offset)
|
||||
position = position @ tranlation_matrix
|
||||
|
||||
# Restore Object rotation to zero
|
||||
local_rot_mat = obj.rotation_euler.to_matrix()
|
||||
rot_mat = Matrix.Rotation(-existing_x_angle, 4, "X")
|
||||
new_rot_mat = local_rot_mat.to_4x4() @ rot_mat
|
||||
new_rot_euler = new_rot_mat.to_euler()
|
||||
obj.rotation_euler = new_rot_euler
|
||||
# For AXIS3 with dual rotation: Reset rotation to zero so profile is horizontal
|
||||
if usage_type == "LAYER3":
|
||||
# Store original rotation for later restoration
|
||||
original_rotation_x = obj.rotation_euler.x
|
||||
obj["pre_edit_rotation_x"] = original_rotation_x
|
||||
|
||||
# Reset rotation to zero - profile will be horizontal
|
||||
current_z_rot = obj.rotation_euler.z
|
||||
obj.rotation_euler.x = 0.0
|
||||
obj.rotation_euler.z = current_z_rot
|
||||
else:
|
||||
# Original behavior: Restore Object rotation to zero
|
||||
local_rot_mat = obj.rotation_euler.to_matrix()
|
||||
rot_mat = Matrix.Rotation(-existing_x_angle, 4, "X")
|
||||
new_rot_mat = local_rot_mat.to_4x4() @ rot_mat
|
||||
new_rot_euler = new_rot_mat.to_euler()
|
||||
obj.rotation_euler = new_rot_euler
|
||||
|
||||
else:
|
||||
position = Matrix()
|
||||
|
||||
tool.Model.import_profile(extrusion.SweptArea, obj=obj, position=position, x_angle=existing_x_angle)
|
||||
# Import profile with correct x_angle
|
||||
if usage_type == "LAYER3":
|
||||
# For LAYER3: Use x_angle=0 and scale by cos(rotation) to get horizontal projection
|
||||
obj_x_rotation = original_rotation_x # Use stored original rotation
|
||||
scale_factor = abs(cos(obj_x_rotation)) if abs(obj_x_rotation) > 1e-6 else 1.0
|
||||
|
||||
# Import with x_angle=0
|
||||
tool.Model.import_profile(extrusion.SweptArea, obj=obj, position=position, x_angle=0)
|
||||
|
||||
# Scale the Y coordinates by cos(rotation) to get horizontal projection
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
for vert in obj.data.vertices:
|
||||
vert.co.y *= scale_factor
|
||||
else:
|
||||
# For other types: Use existing_x_angle
|
||||
tool.Model.import_profile(extrusion.SweptArea, obj=obj, position=position, x_angle=existing_x_angle)
|
||||
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_extrusion_profile(context))
|
||||
if not bpy.app.background:
|
||||
tool.Blender.set_viewport_tool("bim.cad_tool")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -706,6 +857,8 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
extrusion = tool.Model.get_extrusion(body)
|
||||
existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
|
||||
layer_params = tool.Model.get_material_layer_parameters(element)
|
||||
usage_type = tool.Model.get_usage_type(element)
|
||||
|
||||
if extrusion.Position:
|
||||
position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist())
|
||||
position.translation *= self.unit_scale
|
||||
@@ -718,20 +871,40 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tranlation_matrix = Matrix.Translation(rot_offset)
|
||||
position = position @ tranlation_matrix
|
||||
|
||||
# Restore Object rotation to x_angle
|
||||
local_rot_mat = obj.rotation_euler.to_matrix()
|
||||
rot_mat = Matrix.Rotation(existing_x_angle, 4, "X")
|
||||
new_rot_mat = local_rot_mat.to_4x4() @ rot_mat
|
||||
new_rot_euler = new_rot_mat.to_euler()
|
||||
obj.rotation_euler = new_rot_euler
|
||||
# Restore rotation
|
||||
if usage_type == "LAYER3":
|
||||
# Restore original rotation from before editing
|
||||
if "pre_edit_rotation_x" in obj:
|
||||
current_z_rot = obj.rotation_euler.z
|
||||
obj.rotation_euler.x = obj["pre_edit_rotation_x"]
|
||||
obj.rotation_euler.z = current_z_rot
|
||||
del obj["pre_edit_rotation_x"]
|
||||
else:
|
||||
# Original behavior
|
||||
local_rot_mat = obj.rotation_euler.to_matrix()
|
||||
rot_mat = Matrix.Rotation(existing_x_angle, 4, "X")
|
||||
new_rot_mat = local_rot_mat.to_4x4() @ rot_mat
|
||||
new_rot_euler = new_rot_mat.to_euler()
|
||||
obj.rotation_euler = new_rot_euler
|
||||
|
||||
else:
|
||||
position = Matrix()
|
||||
|
||||
profile = tool.Model.export_profile(obj, position=position, x_angle=existing_x_angle)
|
||||
# Export profile with correct x_angle
|
||||
if usage_type == "LAYER3":
|
||||
# Scale Y coordinates back up before exporting
|
||||
obj_x_rotation = obj.rotation_euler.x
|
||||
scale_factor = abs(cos(obj_x_rotation)) if abs(obj_x_rotation) > 1e-6 else 1.0
|
||||
|
||||
# Un-scale the profile before exporting
|
||||
for vert in obj.data.vertices:
|
||||
vert.co.y /= scale_factor # Inverse of import scaling
|
||||
|
||||
profile = tool.Model.export_profile(obj, position=position, x_angle=0)
|
||||
else:
|
||||
profile = tool.Model.export_profile(obj, position=position, x_angle=existing_x_angle)
|
||||
|
||||
if not profile:
|
||||
|
||||
def msg(self, context):
|
||||
self.layout.label(text="INVALID PROFILE")
|
||||
|
||||
@@ -781,6 +954,29 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
|
||||
|
||||
footprint_context = ifcopenshell.util.representation.get_context(
|
||||
tool.Ifc.get(), "Plan", "FootPrint", "SKETCH_VIEW"
|
||||
)
|
||||
if not footprint_context:
|
||||
return
|
||||
|
||||
curves = [profile.OuterCurve]
|
||||
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
|
||||
curves.extend(profile.InnerCurves)
|
||||
new_footprint = ifcopenshell.api.geometry.add_footprint_representation(
|
||||
tool.Ifc.get(), context=footprint_context, curves=curves
|
||||
)
|
||||
old_footprint = ifcopenshell.util.representation.get_representation(element, "Plan", "FootPrint", "SKETCH_VIEW")
|
||||
if old_footprint:
|
||||
for inverse in tool.Ifc.get().get_inverse(old_footprint):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_footprint, new_footprint)
|
||||
bonsai.core.geometry.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_footprint)
|
||||
else:
|
||||
ifcopenshell.api.geometry.assign_representation(
|
||||
tool.Ifc.get(), product=element, representation=new_footprint
|
||||
)
|
||||
|
||||
|
||||
class ResetVertex(bpy.types.Operator):
|
||||
bl_idname = "bim.reset_vertex"
|
||||
bl_label = "Reset Vertex"
|
||||
|
||||
Reference in New Issue
Block a user