Compare commits

...

1 Commits

Author SHA1 Message Date
Ryan Schultz d8da01c877 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
2026-01-11 17:39:54 -06:00
6 changed files with 451 additions and 132 deletions
+254 -58
View File
@@ -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"
+107 -47
View File
@@ -43,7 +43,7 @@ import bonsai.core.geometry
import bonsai.core.model as core
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from math import pi, sin, cos, degrees, atan2
from math import pi, sin, cos, degrees, atan2, acos
from mathutils import Vector, Matrix
from bonsai.bim.module.model.opening import FilledOpeningGenerator
from bonsai.bim.module.model.decorator import PolylineDecorator, ProductDecorator
@@ -397,27 +397,46 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
for obj in selected_objs:
element = tool.Ifc.get_entity(obj)
assert element
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
continue
extrusion = tool.Model.get_extrusion(representation)
if not extrusion:
continue
# Get extrusion direction
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
# Calculate angle from vertical
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
extrusion.Depth = self.depth / si_conversion * (1 / cos(x_angle))
# For sloped walls, compensate so VERTICAL height = target depth
cos_angle = cos(x_angle)
compensation_factor = abs(1 / cos_angle) if abs(cos_angle) > 1e-6 else 1.0
new_depth_ifc = (self.depth / si_conversion) * compensation_factor
extrusion.Depth = new_depth_ifc
# IMPORTANT: Refresh the geometry to reflect the IFC changes
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
)
if tool.Model.get_usage_type(element) == "LAYER2":
for rel in element.ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements":
ifcopenshell.api.geometry.disconnect_element(
ifc_file,
relating_element=rel.RelatingElement,
related_element=element,
)
layer2_objs.append(obj)
related_element = rel.RelatedElement
if related_element.is_a() == "IfcWall":
layer2_objs.append(tool.Ifc.get_object(related_element))
if layer2_objs:
tool.Model.recalculate_walls(layer2_objs)
return {"FINISHED"}
@@ -437,80 +456,126 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
layer2_objs: list[bpy.types.Object] = []
x_angle = 0 if tool.Cad.is_x(self.x_angle, 0, tolerance=0.001) else self.x_angle
x_angle = 0 if tool.Cad.is_x(self.x_angle, pi, tolerance=0.001) else self.x_angle
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
selected_objs = tool.Model.get_selected_mesh_ifc_objects()
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
x_angle = self.x_angle
for obj in selected_objs:
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
assert element
if not element:
continue
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
continue
extrusion = tool.Model.get_extrusion(representation)
if not extrusion:
continue
# Get current object rotation matrix
obj_rotation = obj.matrix_world.to_3x3()
# Get current extrusion direction in LOCAL coordinates
current_local_direction = Vector(extrusion.ExtrudedDirection.DirectionRatios)
if current_local_direction.length == 0:
current_local_direction = Vector((0, 0, 1))
current_local_direction_normalized = current_local_direction.normalized()
# Calculate what the current extrusion direction is in WORLD coordinates
current_world_direction = obj_rotation @ current_local_direction_normalized
existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
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
# Calculate the NEW local extrusion direction based on x_angle
new_local_direction = Vector((0.0, sin(x_angle), cos(x_angle)))
# Check if extrusion direction is actually changing
current_local_norm = current_local_direction_normalized
new_local_norm = new_local_direction.normalized()
# Compare the LOCAL directions
local_direction_changed = (new_local_norm - current_local_norm).length > 1e-6
if tool.Model.get_usage_type(element) == "LAYER2":
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
depth = extrusion.Depth / abs(1 / cos(existing_x_angle))
perpendicular_depth = depth * abs(1 / cos(x_angle))
extrusion.ExtrudedDirection.DirectionRatios = (0.0, sin(x_angle), cos(x_angle))
layer2_objs.append(obj)
# Update extrusion direction
if local_direction_changed:
extrusion.ExtrudedDirection.DirectionRatios = tuple(new_local_direction)
# Always update depth
extrusion.Depth = perpendicular_depth
layer2_objs.append(obj)
else:
if tool.Model.get_usage_type(element) == "LAYER3":
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
# For slabs, handle polyline scaling
existing_obj_x_angle = obj.rotation_euler.x
existing_obj_x_angle = 0 if tool.Cad.is_x(existing_obj_x_angle, 0, tolerance=0.001) else existing_obj_x_angle
existing_obj_x_angle = 0 if tool.Cad.is_x(existing_obj_x_angle, pi, tolerance=0.001) else existing_obj_x_angle
# Scale the polyline coordinates
coord_list = builder.get_polyline_coords(extrusion.SweptArea.OuterCurve)
coord_list = [
(p[0], p[1] * abs(cos(existing_x_angle))) for p in coord_list
] # Reset the transformation and returns to the original points with 0 degrees
] # Reset the transformation
coord_list = [
(p[0], p[1] * abs(1 / cos(x_angle))) for p in coord_list
] # Apply the transformation for the new x_angle
builder.set_polyline_coords(extrusion.SweptArea.OuterCurve, coord_list)
# The extrusion direction calculated previously default to the positive direction
# Here we set the extrusion direction to negative if that's the case
direction_ratios = Vector((0.0, sin(x_angle), cos(x_angle)))
# direction_ratios = Vector(extrusion.ExtrudedDirection.DirectionRatios)
# Calculate new extrusion direction with direction sense
base_local_direction = Vector((0.0, sin(x_angle), cos(x_angle)))
layer_params = tool.Model.get_material_layer_parameters(element)
perpendicular_depth = layer_params["thickness"] * abs(1 / cos(x_angle)) / unit_scale
perpendicular_offset = layer_params["offset"] * abs(1 / cos(x_angle)) / unit_scale
offset_direction = direction_ratios.copy()
offset_direction = base_local_direction.copy()
# Check angle and z direction to determine whether the extrusion direction is positive or negative
if (abs(x_angle) < (pi / 2) and direction_ratios.z > 0) or (
abs(x_angle) > (pi / 2) and direction_ratios.z < 0
# Apply direction sense
final_local_direction = base_local_direction.copy()
if (abs(x_angle) < (pi / 2) and base_local_direction.z > 0) or (
abs(x_angle) > (pi / 2) and base_local_direction.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 ((x_angle) > (pi / 2) and direction_ratios.z > 0) or (
(x_angle) < (pi / 2) and direction_ratios.z < 0
final_local_direction *= -1
elif (x_angle > (pi / 2) and base_local_direction.z > 0) or (
x_angle < (pi / 2) and base_local_direction.z < 0
):
# The extrusion direction is negative. If the layer_parameter is set to positive,
# then the we change the extrusion direction.
# 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)
final_local_direction *= -1
# Check if extrusion direction actually changed
final_local_norm = final_local_direction.normalized()
local_direction_changed = (final_local_norm - current_local_norm).length > 1e-6
# Update extrusion properties
extrusion.ExtrudedDirection.DirectionRatios = tuple(final_local_direction)
extrusion.Depth = perpendicular_depth
if extrusion.Position or perpendicular_offset != 0:
position = offset_direction * perpendicular_offset
tool.Model.add_extrusion_position(extrusion, position)
# Adjust object rotation if extrusion direction changed
if local_direction_changed:
# Calculate what the NEW world direction would be with current object rotation
expected_new_world_direction = obj_rotation @ final_local_norm
# The rotation needed is from expected_new_world_direction to current_world_direction
rotation_axis = expected_new_world_direction.cross(current_world_direction)
if rotation_axis.length > 1e-6:
rotation_axis.normalize()
dot_product = expected_new_world_direction.dot(current_world_direction)
angle = acos(min(max(dot_product, -1), 1))
# Create and apply rotation matrix
rotation_matrix = Matrix.Rotation(angle, 4, rotation_axis)
obj.matrix_world = rotation_matrix @ obj.matrix_world
bpy.context.view_layer.update()
bonsai.core.geometry.switch_representation(
tool.Ifc,
@@ -519,12 +584,6 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
representation=representation,
)
# Object rotation
current_z_rot = obj.rotation_euler.z
rot_mat = mathutils.Matrix.Rotation(x_angle, 4, "X")
obj.rotation_euler = rot_mat.to_euler()
obj.rotation_euler.z = current_z_rot
if layer2_objs:
tool.Model.recalculate_walls(layer2_objs)
return {"FINISHED"}
@@ -1022,6 +1081,7 @@ class DumbWallGenerator:
obj=obj,
representation=representation,
)
pset = ifcopenshell.api.pset.add_pset(self.file, product=element, name="EPset_Parametric")
ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties={"Engine": "Bonsai.DumbLayer2"})
material = ifcopenshell.util.element.get_material(element)
-2
View File
@@ -44,8 +44,6 @@ class Collector(bonsai.core.tool.Collector):
# Note that tool.Geometry.is_locked is only checked within the if
# statements for efficiency as it is a slow check.
tool.Geometry.lock_scale(obj)
if element.is_a("IfcSlab"):
tool.Geometry.lock_rotation(obj, x=True)
if element.is_a("IfcGridAxis"):
if tool.Geometry.is_locked(element):
+63 -7
View File
@@ -1030,28 +1030,57 @@ class Loader(bonsai.core.tool.Loader):
sense_factor = 1
else:
return mesh
if len(layer_set.MaterialLayers) == 1:
return mesh
bm = bmesh.new()
bm.from_mesh(mesh)
prev_co = None
advance_direction = None # Will store direction to advance planes
if not usage:
sense_factor = 1 # Assume the extrusion vector points in the direction sense
sense_factor = 1
no = cls.get_extrusion_vector(element).normalized()
co = Vector((0.0, 0.0, offset))
advance_direction = no
elif usage.LayerSetDirection == "AXIS2":
co = Vector((0.0, offset, 0.0))
no = cls.get_extrusion_vector(element).normalized()
no = no.cross(Vector([1.0, 0.0, 0.0]))
# 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:
while item.is_a("IfcBooleanResult"):
item = item.FirstOperand
if item.is_a("IfcExtrudedAreaSolid"):
local_extrusion = Vector(item.ExtrudedDirection.DirectionRatios).normalized()
break
# 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
advance_direction = thickness_dir
elif usage.LayerSetDirection == "AXIS3":
co = Vector((0.0, 0.0, offset))
no = cls.get_extrusion_vector(element).normalized()
no = Vector([0.0, 0.0, 1.0])
advance_direction = no
elif usage.LayerSetDirection == "AXIS1":
co = Vector((0.0, 0.0, offset))
no = cls.get_extrusion_vector(element).normalized()
no = Vector([1.0, 0.0, 0.0])
advance_direction = no
no *= sense_factor
advance_direction *= sense_factor
# Cache this
body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
styles = {}
@@ -1059,20 +1088,25 @@ class Loader(bonsai.core.tool.Loader):
for i, material in enumerate(mesh.materials):
if style := tool.Ifc.get_entity(material):
styles[style] = i
last_i = len(layer_set.MaterialLayers) - 1
for i, layer in enumerate(layer_set.MaterialLayers):
if i != last_i:
prev_co = co.copy()
co += no * layer.LayerThickness * cls.unit_scale
# Use advance_direction (not no) to move planes!
co += advance_direction * layer.LayerThickness * cls.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
)
bmesh.ops.duplicate(bm, geom=bisect_geom["geom_cut"])
if not (style := ifcopenshell.util.representation.get_material_style(layer.Material, body)):
continue
if (material_index := styles.get(style, None)) is None:
material_index = len(mesh.materials)
mesh.materials.append(tool.Ifc.get_object(style))
if i == last_i:
for face in bisect_geom["geom"]:
if isinstance(face, bmesh.types.BMFace):
@@ -1097,13 +1131,35 @@ class Loader(bonsai.core.tool.Loader):
return mesh
@classmethod
def get_extrusion_vector(cls, wall):
if body := ifcopenshell.util.representation.get_representation(wall, "Model", "Body", "MODEL_VIEW"):
def get_extrusion_vector(cls, element):
"""Get the extrusion direction in WORLD coordinates (accounting for object rotation)"""
if body := ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW"):
for item in ifcopenshell.util.representation.resolve_representation(body).Items:
while item.is_a("IfcBooleanResult"):
item = item.FirstOperand
if item.is_a("IfcExtrudedAreaSolid"):
return Vector(item.ExtrudedDirection.DirectionRatios)
local_direction = Vector(item.ExtrudedDirection.DirectionRatios)
# Transform to world coordinates using object rotation
obj = tool.Ifc.get_object(element)
if obj:
# Apply object rotation to get actual world direction
world_direction = obj.matrix_world.to_3x3() @ local_direction
return world_direction
return local_direction
return Vector([0.0, 0.0, 1.0])
@classmethod
def get_local_extrusion_vector(cls, element):
"""Get the extrusion direction in LOCAL coordinates (from IFC, no object rotation)"""
if body := ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW"):
for item in ifcopenshell.util.representation.resolve_representation(body).Items:
while item.is_a("IfcBooleanResult"):
item = item.FirstOperand
if item.is_a("IfcExtrudedAreaSolid"):
local_direction = Vector(item.ExtrudedDirection.DirectionRatios)
return local_direction
return Vector([0.0, 0.0, 1.0])
@classmethod
@@ -100,35 +100,45 @@ class Usecase:
size = self.convert_si_to_unit(1)
points = ((0.0, 0.0), (size, 0.0), (size, size), (0.0, size), (0.0, 0.0))
if self.polyline:
points = [
(self.convert_si_to_unit(p[0]), self.convert_si_to_unit(p[1] * abs(1 / cos(self.x_angle))))
for p in self.polyline
]
# Only scale polyline if we have actual slope
if self.x_angle and abs(self.x_angle) > 1e-6:
points = [
(self.convert_si_to_unit(p[0]), self.convert_si_to_unit(p[1] * abs(1 / cos(self.x_angle))))
for p in self.polyline
]
else:
points = [
(self.convert_si_to_unit(p[0]), self.convert_si_to_unit(p[1]))
for p in self.polyline
]
if self.file.schema == "IFC2X3":
curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in points])
else:
curve = self.file.createIfcIndexedPolyCurve(self.file.createIfcCartesianPointList2D(points))
if self.x_angle:
direction_ratios = (0.0, sin(self.x_angle), cos(self.x_angle))
else:
direction_ratios = (0.0, 0.0, 1.0)
offset_direction = direction_ratios # offset direction doesn't change if direction_sense is negative
extrusion_direction = self.file.createIfcDirection(direction_ratios)
if self.direction_sense == "NEGATIVE":
direction_ratios = tuple(-n for n in direction_ratios)
extrusion_direction = self.file.createIfcDirection(direction_ratios)
perpendicular_offset = self.convert_si_to_unit(self.offset) * abs(1 / cos(self.x_angle))
perpendicular_depth = self.convert_si_to_unit(self.depth) * abs(1 / cos(self.x_angle))
# Calculate depth based on extrusion angle
extrusion_angle = abs(self.x_angle) if self.x_angle else 0
if extrusion_angle > 1e-6:
perpendicular_depth = self.convert_si_to_unit(self.depth) * abs(1 / cos(extrusion_angle))
perpendicular_offset = self.convert_si_to_unit(self.offset) * abs(1 / cos(extrusion_angle))
else:
perpendicular_depth = self.convert_si_to_unit(self.depth)
perpendicular_offset = self.convert_si_to_unit(self.offset)
position = None
# default position for IFC2X3 where .Position is not optional
if self.file.schema == "IFC2X3" or self.offset != 0:
position_vector = (
offset_direction[0] * perpendicular_offset,
offset_direction[1] * perpendicular_offset,
offset_direction[2] * perpendicular_offset,
direction_ratios[0] * perpendicular_offset,
direction_ratios[1] * perpendicular_offset,
direction_ratios[2] * perpendicular_offset,
)
position = self.file.createIfcAxis2Placement3D(
self.file.createIfcCartesianPoint(position_vector),
@@ -85,7 +85,6 @@ class Usecase:
def create_item(self) -> ifcopenshell.entity_instance:
length = self.convert_si_to_unit(self.settings["length"])
thickness = self.convert_si_to_unit(self.settings["thickness"])
thickness *= 1 / cos(self.settings["x_angle"])
if self.settings["direction_sense"] == "NEGATIVE":
thickness *= -1
points = (
@@ -113,7 +112,7 @@ class Usecase:
self.file.createIfcDirection((1.0, 0.0, 0.0)),
),
extrusion_direction,
self.convert_si_to_unit(self.settings["height"]) * abs(1 / cos(self.settings["x_angle"])),
self.convert_si_to_unit(self.settings["height"]),
)
if self.settings["booleans"]:
extrusion = self.apply_booleans(extrusion)