Merge remote-tracking branch 'upstream/v0.8.0' into MEP-Segment-tool-add-rectangular-hollow-profile

This commit is contained in:
falken10vdl
2026-01-29 13:17:53 +01:00
15 changed files with 309 additions and 659 deletions
@@ -5056,7 +5056,7 @@ class ShowCategoryHelp(bpy.types.Operator):
class AddElementValueRow(bpy.types.Operator): class AddElementValueRow(bpy.types.Operator):
bl_idname = "bim.add_element_value_row" bl_idname = "bim.add_element_value_row"
bl_label = "Add Element" bl_label = "Add Element Value Row"
bl_description = "Add a new element value row" bl_description = "Add a new element value row"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
+86 -245
View File
@@ -40,11 +40,9 @@ import bonsai.core.root
import bonsai.core.type import bonsai.core.type
import bonsai.tool as tool import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.model.decorator import ( from math import cos, pi
PolylineDecorator, from mathutils import Vector, Matrix
ProductDecorator, from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator
ProfileDecorator,
)
from bonsai.bim.module.model.polyline import PolylineOperator from bonsai.bim.module.model.polyline import PolylineOperator
@@ -231,17 +229,16 @@ class DumbSlabPlaner:
for inverse in tool.Ifc.get().get_inverse(layer_set): for inverse in tool.Ifc.get().get_inverse(layer_set):
if not inverse.is_a("IfcMaterialLayerSetUsage") or inverse.LayerSetDirection != "AXIS3": if not inverse.is_a("IfcMaterialLayerSetUsage") or inverse.LayerSetDirection != "AXIS3":
continue continue
if tool.Ifc.get().schema == "IFC2X3": if tool.Ifc.get().schema == "IFC2X3":
for rel in tool.Ifc.get().get_inverse(inverse): for rel in tool.Ifc.get().get_inverse(inverse):
if not rel.is_a("IfcRelAssociatesMaterial"): if not rel.is_a("IfcRelAssociatesMaterial"):
continue continue
for element in rel.RelatedObjects: for element in rel.RelatedObjects:
self.change_thickness(element, total_thickness, preserve_offset=True) self.change_thickness(element, total_thickness)
else: else:
for rel in inverse.AssociatedTo: for rel in inverse.AssociatedTo:
for element in rel.RelatedObjects: for element in rel.RelatedObjects:
self.change_thickness(element, total_thickness, preserve_offset=True) self.change_thickness(element, total_thickness)
def regenerate_from_occurence(self, element, material_set_usage): def regenerate_from_occurence(self, element, material_set_usage):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
@@ -251,12 +248,10 @@ class DumbSlabPlaner:
return return
self.change_thickness(element, total_thickness) self.change_thickness(element, total_thickness)
def change_thickness( def change_thickness(self, element: ifcopenshell.entity_instance, thickness: float) -> None:
self, element: ifcopenshell.entity_instance, thickness: float, preserve_offset: bool = False self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
) -> None:
if tool.Model.get_usage_type(element) != "LAYER3": if tool.Model.get_usage_type(element) != "LAYER3":
return return
layer_params = tool.Model.get_material_layer_parameters(element) layer_params = tool.Model.get_material_layer_parameters(element)
ifc_file = tool.Ifc.get() ifc_file = tool.Ifc.get()
body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
@@ -274,48 +269,72 @@ class DumbSlabPlaner:
if representation: if representation:
extrusion = tool.Model.get_extrusion(representation) extrusion = tool.Model.get_extrusion(representation)
if extrusion: 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) 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
# Calculate the actual extrusion angle from vertical # Check angle and z direction to determine whether the extrusion direction is positive or negative
extrusion_angle = 0 if (abs(existing_x_angle) < (pi / 2) and direction_ratios.z > 0) or (
if direction_ratios.length > 0: abs(existing_x_angle) > (pi / 2) and direction_ratios.z < 0
cos_angle = direction_ratios.normalized().dot(Vector((0, 0, 1))) ):
extrusion_angle = acos(min(max(cos_angle, -1), 1)) # The extrusion direction is positive. If the layer_parameter is set to negative,
# then the we change the extrusion direction.
# Only apply 1/cos factor when there's actual extrusion slope if layer_params["direction_sense"] == "NEGATIVE":
if extrusion_angle > 1e-6: direction_ratios *= -1
perpendicular_depth = thickness * abs(1 / cos(extrusion_angle)) elif (abs(existing_x_angle) > (pi / 2) and direction_ratios.z > 0) or (
perpendicular_offset = layer_offset * abs(1 / cos(extrusion_angle)) abs(existing_x_angle) < (pi / 2) and direction_ratios.z < 0
else: ):
perpendicular_depth = thickness # The extrusion direction is negative. If the layer_parameter is set to positive,
perpendicular_offset = layer_offset # 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 extrusion.Depth = perpendicular_depth
# Update position
ifc_position = extrusion.Position 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
else:
tool.Model.add_extrusion_position(extrusion, position)
if direction_ratios.length > 0: else:
offset_vector = direction_ratios.normalized() * perpendicular_offset props = tool.Model.get_model_props()
position = offset_vector 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(
material = ifcopenshell.util.element.get_material(element) tool.Ifc.get(),
if material and material.is_a("IfcMaterialLayerSetUsage"): context=body_context,
# Only set offset if not preserving it (preserves independent offsets per instance) depth=thickness * self.unit_scale,
if not preserve_offset: x_angle=x_angle,
material.OffsetFromReferenceLine = position.z )
for inverse in tool.Ifc.get().get_inverse(representation):
if ifc_position: ifcopenshell.util.element.replace_attribute(inverse, representation, new_rep)
ifc_position.Location.Coordinates = position bonsai.core.geometry.switch_representation(
else: tool.Ifc,
tool.Model.add_extrusion_position(extrusion, position) tool.Geometry,
obj=obj,
bonsai.core.geometry.switch_representation( representation=new_rep,
tool.Ifc, )
tool.Geometry, bonsai.core.geometry.remove_representation(
obj=obj, tool.Ifc, tool.Geometry, obj=obj, representation=representation
representation=representation, )
) return
else: else:
props = tool.Model.get_model_props() 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 x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else props.x_angle
@@ -328,118 +347,13 @@ class DumbSlabPlaner:
ifcopenshell.api.geometry.assign_representation( ifcopenshell.api.geometry.assign_representation(
tool.Ifc.get(), product=element, representation=representation tool.Ifc.get(), product=element, representation=representation
) )
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
)
def update_extrusion_direction( bonsai.core.geometry.switch_representation(
element: ifcopenshell.entity_instance, new_direction_ratios: tuple, obj: bpy.types.Object = None tool.Ifc,
) -> None: tool.Geometry,
""" obj=obj,
Update extrusion direction while preserving overall object orientation. representation=representation,
)
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): class EnableEditingSketchExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
@@ -716,8 +630,6 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
existing_x_angle = tool.Model.get_existing_x_angle(extrusion) existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
layer_params = tool.Model.get_material_layer_parameters(element) layer_params = tool.Model.get_material_layer_parameters(element)
usage_type = tool.Model.get_usage_type(element)
# TODO: review #7537 properly, this is a quick fix but something doesn't seem right. # TODO: review #7537 properly, this is a quick fix but something doesn't seem right.
original_rotation_x = 0 original_rotation_x = 0
if extrusion.Position: if extrusion.Position:
@@ -732,49 +644,22 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
tranlation_matrix = Matrix.Translation(rot_offset) tranlation_matrix = Matrix.Translation(rot_offset)
position = position @ tranlation_matrix position = position @ tranlation_matrix
# For AXIS3 with dual rotation: Reset rotation to zero so profile is horizontal # Restore Object rotation to zero
if usage_type == "LAYER3": local_rot_mat = obj.rotation_euler.to_matrix()
# Store original rotation for later restoration rot_mat = Matrix.Rotation(-existing_x_angle, 4, "X")
original_rotation_x = obj.rotation_euler.x new_rot_mat = local_rot_mat.to_4x4() @ rot_mat
obj["pre_edit_rotation_x"] = original_rotation_x new_rot_euler = new_rot_mat.to_euler()
obj.rotation_euler = new_rot_euler
# 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: else:
position = Matrix() position = Matrix()
# Import profile with correct x_angle tool.Model.import_profile(extrusion.SweptArea, obj=obj, position=position, x_angle=existing_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") bpy.ops.object.mode_set(mode="EDIT")
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_extrusion_profile(context)) ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_extrusion_profile(context))
if not bpy.app.background: if not bpy.app.background:
tool.Blender.set_viewport_tool("bim.cad_tool") tool.Blender.set_viewport_tool("bim.cad_tool")
return {"FINISHED"} return {"FINISHED"}
@@ -796,7 +681,6 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
extrusion = tool.Model.get_extrusion(body) extrusion = tool.Model.get_extrusion(body)
existing_x_angle = tool.Model.get_existing_x_angle(extrusion) existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
layer_params = tool.Model.get_material_layer_parameters(element) layer_params = tool.Model.get_material_layer_parameters(element)
usage_type = tool.Model.get_usage_type(element)
if extrusion.Position: if extrusion.Position:
position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist()) position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist())
@@ -810,38 +694,17 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
tranlation_matrix = Matrix.Translation(rot_offset) tranlation_matrix = Matrix.Translation(rot_offset)
position = position @ tranlation_matrix position = position @ tranlation_matrix
# Restore rotation # Restore Object rotation to x_angle
if usage_type == "LAYER3": local_rot_mat = obj.rotation_euler.to_matrix()
# Restore original rotation from before editing rot_mat = Matrix.Rotation(existing_x_angle, 4, "X")
if "pre_edit_rotation_x" in obj: new_rot_mat = local_rot_mat.to_4x4() @ rot_mat
current_z_rot = obj.rotation_euler.z new_rot_euler = new_rot_mat.to_euler()
obj.rotation_euler.x = obj["pre_edit_rotation_x"] obj.rotation_euler = new_rot_euler
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: else:
position = Matrix() position = Matrix()
# Export profile with correct x_angle profile = tool.Model.export_profile(obj, position=position, x_angle=existing_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: if not profile:
@@ -893,28 +756,6 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
tool.Ifc.get(), product=element, representation=new_footprint tool.Ifc.get(), product=element, representation=new_footprint
) )
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): class ResetVertex(bpy.types.Operator):
bl_idname = "bim.reset_vertex" bl_idname = "bim.reset_vertex"
+47 -120
View File
@@ -48,6 +48,8 @@ import bonsai.core.root
import bonsai.core.type import bonsai.core.type
import bonsai.tool as tool import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore from bonsai.bim.ifc import IfcStore
from math import pi, sin, cos, degrees, atan2
from mathutils import Vector, Matrix
from bonsai.bim.module.model.decorator import PolylineDecorator, ProductDecorator from bonsai.bim.module.model.decorator import PolylineDecorator, ProductDecorator
from bonsai.bim.module.model.opening import FilledOpeningGenerator from bonsai.bim.module.model.opening import FilledOpeningGenerator
from bonsai.bim.module.model.polyline import PolylineOperator from bonsai.bim.module.model.polyline import PolylineOperator
@@ -403,42 +405,24 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation: if not representation:
continue continue
extrusion = tool.Model.get_extrusion(representation) extrusion = tool.Model.get_extrusion(representation)
if not extrusion: if not extrusion:
continue continue
# Get extrusion direction
x, y, z = extrusion.ExtrudedDirection.DirectionRatios x, y, z = extrusion.ExtrudedDirection.DirectionRatios
# Calculate angle from vertical
x_angle = Vector((0, 1)).angle_signed(Vector((y, z))) 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": if tool.Model.get_usage_type(element) == "LAYER2":
for rel in element.ConnectedFrom: for rel in element.ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements": if rel.is_a() == "IfcRelConnectsElements":
related_element = rel.RelatedElement ifcopenshell.api.geometry.disconnect_element(
if related_element.is_a() == "IfcWall": ifc_file,
layer2_objs.append(tool.Ifc.get_object(related_element)) relating_element=rel.RelatingElement,
related_element=element,
)
layer2_objs.append(obj)
if layer2_objs: if layer2_objs:
tool.Model.recalculate_walls(layer2_objs) tool.Model.recalculate_walls(layer2_objs)
return {"FINISHED"} return {"FINISHED"}
@@ -458,143 +442,81 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context): def _execute(self, context):
layer2_objs: list[bpy.types.Object] = [] layer2_objs: list[bpy.types.Object] = []
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get()) 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()) unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
x_angle = self.x_angle selected_objs = tool.Model.get_selected_mesh_ifc_objects()
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
for obj in context.selected_objects: for obj in selected_objs:
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
if not element: assert element
continue
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation: if not representation:
continue continue
extrusion = tool.Model.get_extrusion(representation) extrusion = tool.Model.get_extrusion(representation)
if not extrusion: if not extrusion:
continue 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 = 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, 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, 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": if tool.Model.get_usage_type(element) == "LAYER2":
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
depth = extrusion.Depth / abs(1 / cos(existing_x_angle)) depth = extrusion.Depth / abs(1 / cos(existing_x_angle))
perpendicular_depth = depth * abs(1 / cos(x_angle)) perpendicular_depth = depth * abs(1 / cos(x_angle))
extrusion.ExtrudedDirection.DirectionRatios = (0.0, sin(x_angle), cos(x_angle))
# 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) layer2_objs.append(obj)
extrusion.Depth = perpendicular_depth
else: else:
if tool.Model.get_usage_type(element) == "LAYER3": if tool.Model.get_usage_type(element) == "LAYER3":
# For slabs, handle polyline scaling existing_x_angle = obj.rotation_euler.x
existing_obj_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_obj_x_angle = ( existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_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 = builder.get_polyline_coords(extrusion.SweptArea.OuterCurve)
coord_list = [ coord_list = [
(p[0], p[1] * abs(cos(existing_x_angle))) for p in coord_list (p[0], p[1] * abs(cos(existing_x_angle))) for p in coord_list
] # Reset the transformation ] # Reset the transformation and returns to the original points with 0 degrees
coord_list = [ coord_list = [
(p[0], p[1] * abs(1 / cos(x_angle))) for p in coord_list (p[0], p[1] * abs(1 / cos(x_angle))) for p in coord_list
] # Apply the transformation for the new x_angle ] # Apply the transformation for the new x_angle
builder.set_polyline_coords(extrusion.SweptArea.OuterCurve, coord_list) builder.set_polyline_coords(extrusion.SweptArea.OuterCurve, coord_list)
# Calculate new extrusion direction with direction sense # The extrusion direction calculated previously default to the positive direction
base_local_direction = Vector((0.0, sin(x_angle), cos(x_angle))) # 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)
layer_params = tool.Model.get_material_layer_parameters(element) layer_params = tool.Model.get_material_layer_parameters(element)
perpendicular_depth = layer_params["thickness"] * abs(1 / cos(x_angle)) / unit_scale perpendicular_depth = layer_params["thickness"] * abs(1 / cos(x_angle)) / unit_scale
perpendicular_offset = layer_params["offset"] * abs(1 / cos(x_angle)) / unit_scale perpendicular_offset = layer_params["offset"] * abs(1 / cos(x_angle)) / unit_scale
offset_direction = base_local_direction.copy() offset_direction = direction_ratios.copy()
# Apply direction sense # Check angle and z direction to determine whether the extrusion direction is positive or negative
final_local_direction = base_local_direction.copy() if (abs(x_angle) < (pi / 2) and direction_ratios.z > 0) or (
if (abs(x_angle) < (pi / 2) and base_local_direction.z > 0) or ( abs(x_angle) > (pi / 2) and direction_ratios.z < 0
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": if layer_params["direction_sense"] == "NEGATIVE":
final_local_direction *= -1 direction_ratios *= -1
elif (x_angle > (pi / 2) and base_local_direction.z > 0) or ( elif ((x_angle) > (pi / 2) and direction_ratios.z > 0) or (
x_angle < (pi / 2) and base_local_direction.z < 0 (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.
# 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 offset_direction *= -1
if layer_params["direction_sense"] == "POSITIVE": if layer_params["direction_sense"] == "POSITIVE":
final_local_direction *= -1 direction_ratios *= -1
# Check if extrusion direction actually changed extrusion.ExtrudedDirection.DirectionRatios = tuple(direction_ratios)
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 extrusion.Depth = perpendicular_depth
if extrusion.Position or perpendicular_offset != 0: if extrusion.Position or perpendicular_offset != 0:
position = offset_direction * perpendicular_offset position = offset_direction * perpendicular_offset
tool.Model.add_extrusion_position(extrusion, position) 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))
# Rotate around object's own origin
# Decompose the matrix to get translation, rotation, scale
translation, rotation, scale = obj.matrix_world.decompose()
# Create rotation matrix and convert to quaternion
rotation_matrix = Matrix.Rotation(angle, 4, rotation_axis)
rotation_quat = rotation_matrix.to_quaternion()
# Apply rotation to existing rotation (quaternion multiplication)
new_rotation = rotation_quat @ rotation
# Reconstruct matrix_world with same translation, new rotation, same scale
obj.matrix_world = (
Matrix.Translation(translation) @ new_rotation.to_matrix().to_4x4() @ Matrix.Scale(1, 4)
)
bpy.context.view_layer.update()
bonsai.core.geometry.switch_representation( bonsai.core.geometry.switch_representation(
tool.Ifc, tool.Ifc,
tool.Geometry, tool.Geometry,
@@ -602,6 +524,12 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
representation=representation, 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: if layer2_objs:
tool.Model.recalculate_walls(layer2_objs) tool.Model.recalculate_walls(layer2_objs)
return {"FINISHED"} return {"FINISHED"}
@@ -1096,7 +1024,6 @@ class DumbWallGenerator:
obj=obj, obj=obj,
representation=representation, representation=representation,
) )
pset = ifcopenshell.api.pset.add_pset(self.file, product=element, name="EPset_Parametric") 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"}) ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties={"Engine": "Bonsai.DumbLayer2"})
material = ifcopenshell.util.element.get_material(element) material = ifcopenshell.util.element.get_material(element)
+1 -2
View File
@@ -99,8 +99,7 @@ def update_name(self: "BIMContainer", context: bpy.types.Context) -> None:
tool.Spatial.edit_container_name(element, self.name) tool.Spatial.edit_container_name(element, self.name)
if obj := tool.Ifc.get_object(element): if obj := tool.Ifc.get_object(element):
tool.Root.set_object_name(obj, element) tool.Root.set_object_name(obj, element)
if collection := tool.Blender.get_object_bim_props(obj).collection: tool.Collector.assign(obj)
collection.name = f"{element.is_a()}/{element.Name or 'Unnamed'}"
bonsai.bim.handler.refresh_ui_data() bonsai.bim.handler.refresh_ui_data()
+5 -5
View File
@@ -951,11 +951,11 @@ class Spatial:
def get_active_container(cls): pass def get_active_container(cls): pass
def get_container(cls, element): pass def get_container(cls, element): pass
def get_decomposed_elements(cls, container, recursive): pass def get_decomposed_elements(cls, container, recursive): pass
def get_decomposition(cls, element): pass
def get_object_matrix(cls, obj): pass def get_object_matrix(cls, obj): pass
def get_relative_object_matrix(cls, target_obj, relative_to_obj): pass def get_relative_object_matrix(cls, target_obj, relative_to_obj): pass
def get_selected_product_types(cls): pass
def get_root_element(cls, element): pass def get_root_element(cls, element): pass
def get_decomposition(cls, element): pass def get_selected_product_types(cls): pass
def get_selected_products(cls): pass def get_selected_products(cls): pass
def import_spatial_decomposition(cls): pass def import_spatial_decomposition(cls): pass
def import_spatial_element(cls, element, level_index): pass def import_spatial_element(cls, element, level_index): pass
@@ -1125,15 +1125,15 @@ class Unit:
def disable_editing_units(cls): pass def disable_editing_units(cls): pass
def enable_editing_units(cls): pass def enable_editing_units(cls): pass
def export_unit_attributes(cls): pass def export_unit_attributes(cls): pass
def get_currency_name(cls): pass
def get_project_currency_unit(cls): pass
def get_scene_unit_name(cls, unit_type): pass def get_scene_unit_name(cls, unit_type): pass
def get_scene_unit_si_prefix(cls, name): pass def get_scene_unit_si_prefix(cls, name): pass
def import_unit_attributes(cls, unit): pass def import_unit_attributes(cls, unit): pass
def import_units(cls): pass def import_units(cls): pass
def is_scene_unit_metric(cls): pass def is_si_unit(cls, name): pass
def is_unit_class(cls, unit, ifc_class): pass def is_unit_class(cls, unit, ifc_class): pass
def set_active_unit(cls, unit): pass def set_active_unit(cls, unit): pass
def get_project_currency_unit(cls): pass
def get_currency_name(cls): pass
@interface @interface
class Voider: class Voider:
+2
View File
@@ -46,6 +46,8 @@ class Collector(bonsai.core.tool.Collector):
# Note that tool.Geometry.is_locked is only checked within the if # Note that tool.Geometry.is_locked is only checked within the if
# statements for efficiency as it is a slow check. # statements for efficiency as it is a slow check.
tool.Geometry.lock_scale(obj) tool.Geometry.lock_scale(obj)
if element.is_a("IfcSlab"):
tool.Geometry.lock_rotation(obj, x=True)
if element.is_a("IfcGridAxis"): if element.is_a("IfcGridAxis"):
if tool.Geometry.is_locked(element): if tool.Geometry.is_locked(element):
+18 -147
View File
@@ -1024,7 +1024,7 @@ class Loader(bonsai.core.tool.Loader):
elif material.is_a("IfcMaterialLayerSetUsage"): elif material.is_a("IfcMaterialLayerSetUsage"):
usage = material usage = material
layer_set = material.ForLayerSet layer_set = material.ForLayerSet
offset = usage.OffsetFromReferenceLine offset = usage.OffsetFromReferenceLine * cls.unit_scale
sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1 sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1
elif material.is_a("IfcMaterialLayerSet"): elif material.is_a("IfcMaterialLayerSet"):
usage = None usage = None
@@ -1033,168 +1033,61 @@ class Loader(bonsai.core.tool.Loader):
sense_factor = 1 sense_factor = 1
else: else:
return mesh return mesh
if len(layer_set.MaterialLayers) == 1: if len(layer_set.MaterialLayers) == 1:
return mesh 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 = bmesh.new()
bm.from_mesh(mesh) bm.from_mesh(mesh)
prev_co = None prev_co = None
advance_direction = None
if not usage: if not usage:
sense_factor = 1 sense_factor = 1 # Assume the extrusion vector points in the direction sense
no = cls.get_extrusion_vector(element).normalized() no = cls.get_extrusion_vector(element).normalized()
co = Vector((0.0, 0.0, offset)) co = Vector((0.0, 0.0, offset))
advance_direction = no
elif usage.LayerSetDirection == "AXIS2": elif usage.LayerSetDirection == "AXIS2":
# Get local extrusion direction co = Vector((0.0, offset, 0.0))
local_extrusion = Vector([0.0, 0.0, 1.0]) no = cls.get_extrusion_vector(element).normalized()
if body := ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW"): no = no.cross(Vector([1.0, 0.0, 0.0]))
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()
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": elif usage.LayerSetDirection == "AXIS3":
# AXIS3 layers go through slab thickness (local Z) co = Vector((0.0, 0.0, offset))
no = cls.get_extrusion_vector(element).normalized()
no = Vector([0.0, 0.0, 1.0]) 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": elif usage.LayerSetDirection == "AXIS1":
co = Vector((0.0, 0.0, offset)) co = Vector((0.0, 0.0, offset))
no = cls.get_extrusion_vector(element).normalized() no = cls.get_extrusion_vector(element).normalized()
no = Vector([1.0, 0.0, 0.0]) no = Vector([1.0, 0.0, 0.0])
advance_direction = no no *= sense_factor
# Cache this
# 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 material styles
body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
styles = {} styles = {}
has_layer_styles = False has_layer_styles = False
for i, material in enumerate(mesh.materials): for i, material in enumerate(mesh.materials):
if style := tool.Ifc.get_entity(material): if style := tool.Ifc.get_entity(material):
styles[style] = i 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 last_i = len(layer_set.MaterialLayers) - 1
for i, layer in enumerate(layer_set.MaterialLayers):
for idx, (original_i, layer) in enumerate(layer_list): if i != last_i:
if idx != last_i:
prev_co = co.copy() prev_co = co.copy()
advance_vector = advance_direction * layer.LayerThickness * thickness_scale co += no * layer.LayerThickness * cls.unit_scale
co += advance_vector
bisect_geom = bmesh.ops.bisect_plane( bisect_geom = bmesh.ops.bisect_plane(
bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no 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"]) bmesh.ops.duplicate(bm, geom=bisect_geom["geom_cut"])
if not (style := ifcopenshell.util.representation.get_material_style(layer.Material, body)): if not (style := ifcopenshell.util.representation.get_material_style(layer.Material, body)):
continue continue
if (material_index := styles.get(style, None)) is None: if (material_index := styles.get(style, None)) is None:
material_index = len(mesh.materials) material_index = len(mesh.materials)
mesh.materials.append(tool.Ifc.get_object(style)) mesh.materials.append(tool.Ifc.get_object(style))
if i == last_i:
if idx == last_i:
for face in bisect_geom["geom"]: for face in bisect_geom["geom"]:
if isinstance(face, bmesh.types.BMFace): if isinstance(face, bmesh.types.BMFace):
center = face.calc_center_median() center = face.calc_center_median()
if (center - co).dot(test_normal) >= 0: if (center - co).dot(no) >= 0:
face.material_index = material_index face.material_index = material_index
has_layer_styles = True has_layer_styles = True
else: else:
for face in bisect_geom["geom"]: for face in bisect_geom["geom"]:
if isinstance(face, bmesh.types.BMFace): if isinstance(face, bmesh.types.BMFace):
center = face.calc_center_median() center = face.calc_center_median()
if (center - co).dot(test_normal) < 0 and (center - prev_co).dot(test_normal) >= 0: if (center - co).dot(no) < 0 and (center - prev_co).dot(no) >= 0:
face.material_index = material_index face.material_index = material_index
has_layer_styles = True has_layer_styles = True
@@ -1207,35 +1100,13 @@ class Loader(bonsai.core.tool.Loader):
return mesh return mesh
@classmethod @classmethod
def get_extrusion_vector(cls, element): def get_extrusion_vector(cls, wall):
"""Get the extrusion direction in WORLD coordinates (accounting for object rotation)""" if body := ifcopenshell.util.representation.get_representation(wall, "Model", "Body", "MODEL_VIEW"):
if body := ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW"):
for item in ifcopenshell.util.representation.resolve_representation(body).Items: for item in ifcopenshell.util.representation.resolve_representation(body).Items:
while item.is_a("IfcBooleanResult"): while item.is_a("IfcBooleanResult"):
item = item.FirstOperand item = item.FirstOperand
if item.is_a("IfcExtrudedAreaSolid"): if item.is_a("IfcExtrudedAreaSolid"):
local_direction = Vector(item.ExtrudedDirection.DirectionRatios) return 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]) return Vector([0.0, 0.0, 1.0])
@classmethod @classmethod
-5
View File
@@ -419,11 +419,6 @@ class Unit(bonsai.core.tool.Unit):
new.is_assigned = unit in assigned_units new.is_assigned = unit in assigned_units
new.ifc_class = unit.is_a() new.ifc_class = unit.is_a()
@classmethod
def is_scene_unit_metric(cls) -> bool:
assert bpy.context.scene
return bpy.context.scene.unit_settings.system in ["METRIC", "NONE"]
@classmethod @classmethod
def is_unit_class(cls, unit: ifcopenshell.entity_instance, ifc_class: str) -> bool: def is_unit_class(cls, unit: ifcopenshell.entity_instance, ifc_class: str) -> bool:
return unit.is_a(ifc_class) return unit.is_a(ifc_class)
+1 -1
View File
@@ -193,7 +193,7 @@ def main() -> None:
print(f"Downloading {url} -> {filepath}") print(f"Downloading {url} -> {filepath}")
urllib.request.urlretrieve(url, filepath) urllib.request.urlretrieve(url, filepath)
input("Dev environment is all set. 🎉🎉\nPress Enter to continue..." "") input("Dev environment is all set!! \nPress Enter to continue...")
if __name__ == "__main__": if __name__ == "__main__":
@@ -74,6 +74,50 @@ Scenario: Assign container
When I click "CHECKMARK" When I click "CHECKMARK"
Then the object "IfcWall/Cube" is in the collection "IfcSite/My Site" Then the object "IfcWall/Cube" is in the collection "IfcSite/My Site"
Scenario: Assign container - assign an aggregate which also affects children
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
When I press "bim.add_aggregate"
Then the object "IfcElementAssembly/Default_Name" exists
And the object "IfcElementAssembly/Default_Name" is contained in object "IfcBuildingStorey/My Storey"
When I look at the "Spatial Decomposition" panel
And I select the "My Site" item in the "BIM_UL_containers_manager" list
And I click "Set Default"
And the object "IfcWall/Cube" is selected
And I look at the "Spatial Container" panel
And I click "GREASEPENCIL"
And I click "CHECKMARK"
Then the object "IfcWall/Cube" is in the collection "IfcSite/My Site"
And the object "IfcElementAssembly/Default_Name" is in the collection "IfcSite/My Site"
Scenario: Assign container - assign a child which also affects parents
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
When I press "bim.add_aggregate"
Then the object "IfcElementAssembly/Default_Name" exists
And the object "IfcElementAssembly/Default_Name" is contained in object "IfcBuildingStorey/My Storey"
When I look at the "Spatial Decomposition" panel
And I select the "My Site" item in the "BIM_UL_containers_manager" list
And I click "Set Default"
And the object "IfcElementAssembly/Default_Name" is selected
And I look at the "Spatial Container" panel
And I click "GREASEPENCIL"
And I click "CHECKMARK"
Then the object "IfcWall/Cube" is in the collection "IfcSite/My Site"
And the object "IfcElementAssembly/Default_Name" is in the collection "IfcSite/My Site"
Scenario: Copy to container Scenario: Copy to container
Given an empty IFC project Given an empty IFC project
And I add a cube And I add a cube
+18 -1
View File
@@ -110,7 +110,7 @@ Scenario: Assign type - assign to a type with a material layer set, which automa
And the object "IfcWall/Unnamed" has a "100" thick layered material containing the material "Default" And the object "IfcWall/Unnamed" has a "100" thick layered material containing the material "Default"
And the object "IfcWall/Unnamed" dimensions are ".5,.1,.5" And the object "IfcWall/Unnamed" dimensions are ".5,.1,.5"
Scenario: Assign type - assign to a different type with a material layer set Scenario: Assign type - assign to a different type with a LAYER2 material layer set
Given an empty IFC project Given an empty IFC project
And I add a cube And I add a cube
And the object "Cube" is selected And the object "Cube" is selected
@@ -149,6 +149,23 @@ Scenario: Assign type - assign to a different type with a material layer set
Then the object "IfcWall/Cube" has a "200" thick layered material containing the material "Default" Then the object "IfcWall/Cube" has a "200" thick layered material containing the material "Default"
And the object "IfcWall/Cube" dimensions are "1,.2,1" And the object "IfcWall/Cube" dimensions are "1,.2,1"
Scenario: Assign type - assign to a different type with a LAYER3 material layer set
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR200'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
When I press "bim.add_occurrence"
Then the object "IfcSlab/Slab" is an "IfcSlab"
And the object "IfcSlab/Slab" dimensions are "1,1,0.2"
And the object "IfcSlab/Slab" bottom left corner is at "0,0,0"
And the object "IfcSlab/Slab" top right corner is at "1,1,0.2"
When I look at the "Type" panel
And I click "GREASEPENCIL"
And I set the "relating_type" property to "FLR300"
And I click "CHECKMARK"
Then the object "IfcSlab/Slab" dimensions are "1,1,0.3"
Scenario: Assign type - assign to a type with a material profile set Scenario: Assign type - assign to a type with a material profile set
Given an empty IFC project Given an empty IFC project
And I add a cube And I add a cube
+70 -98
View File
@@ -22,119 +22,91 @@ from test.core.bootstrap import ifc, unit
class TestAssignSceneUnits: class TestAssignSceneUnits:
def test_creating_and_assigning_metric_units(self, ifc, unit): def test_creating_and_assigning_metric_units(self, ifc, unit):
unit.is_scene_unit_metric().should_be_called().will_return(True) unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("length_name")
unit.get_scene_unit_si_prefix("LENGTHUNIT").should_be_called().will_return("prefix") unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("area_name")
unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return("prefix") unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("volume_name")
unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return("prefix") unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("mass_name")
unit.add_mass_and_time_units().should_be_called().will_return(True) unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("time_name")
unit.get_scene_unit_si_prefix("MASSUNIT").should_be_called().will_return("KILO") unit.is_si_unit("length_name").should_be_called().will_return(True)
unit.get_scene_unit_si_prefix("TIMEUNIT").should_be_called().will_return(None) unit.is_si_unit("area_name").should_be_called().will_return(True)
unit.is_si_unit("volume_name").should_be_called().will_return(True)
ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="prefix").should_be_called().will_return( unit.is_si_unit("mass_name").should_be_called().will_return(True)
unit.is_si_unit("time_name").should_be_called().will_return(True)
unit.get_scene_unit_si_prefix("length_name").should_be_called().will_return("length_prefix")
unit.get_scene_unit_si_prefix("area_name").should_be_called().will_return("area_prefix")
unit.get_scene_unit_si_prefix("volume_name").should_be_called().will_return("volume_prefix")
unit.get_scene_unit_si_prefix("mass_name").should_be_called().will_return("mass_prefix")
unit.get_scene_unit_si_prefix("time_name").should_be_called().will_return("time_prefix")
ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="length_prefix").should_be_called().will_return(
"lengthunit" "lengthunit"
) )
ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix="prefix").should_be_called().will_return("areaunit") ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix="area_prefix").should_be_called().will_return(
ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix="prefix").should_be_called().will_return( "areaunit"
)
ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix="volume_prefix").should_be_called().will_return(
"volumeunit" "volumeunit"
) )
ifc.run("unit.add_si_unit", unit_type="MASSUNIT", prefix="KILO").should_be_called().will_return("massunit") ifc.run("unit.add_si_unit", unit_type="MASSUNIT", prefix="mass_prefix").should_be_called().will_return(
ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=None).should_be_called().will_return("timeunit") "massunit"
ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") )
ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix="time_prefix").should_be_called().will_return(
"timeunit"
)
ifc.run( ifc.run(
"unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "massunit", "timeunit"]
).should_be_called() ).should_be_called()
subject.assign_scene_units(ifc, unit) subject.assign_scene_units(ifc, unit)
def test_creating_and_assigning_metric_units_without_mass_and_time(self, ifc, unit): def test_creating_and_assigning_only_specified_units(self, ifc, unit):
unit.is_scene_unit_metric().should_be_called().will_return(True) unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("length_name")
unit.get_scene_unit_si_prefix("LENGTHUNIT").should_be_called().will_return("CENTI") unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return(None)
unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return("CENTI") unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return(None)
unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return("CENTI") unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return(None)
unit.add_mass_and_time_units().should_be_called().will_return(False) unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return(None)
ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="CENTI").should_be_called().will_return("lengthunit") unit.is_si_unit("length_name").should_be_called().will_return(True)
ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix="CENTI").should_be_called().will_return("areaunit") unit.get_scene_unit_si_prefix("length_name").should_be_called().will_return("length_prefix")
ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix="CENTI").should_be_called().will_return("volumeunit") ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="length_prefix").should_be_called().will_return(
ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") "lengthunit"
ifc.run("unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit"]).should_be_called() )
ifc.run("unit.assign_unit", units=["lengthunit"]).should_be_called()
subject.assign_scene_units(ifc, unit) subject.assign_scene_units(ifc, unit)
def test_creating_and_assigning_imperial_units(self, ifc, unit): def test_creating_and_assigning_imperial_units(self, ifc, unit):
unit.is_scene_unit_metric().should_be_called().will_return(False) unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("length_name")
unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("foot") unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("area_name")
unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square foot") unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("volume_name")
unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic foot") unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("mass_name")
unit.add_mass_and_time_units().should_be_called().will_return(True) unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("time_name")
unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("pound") unit.is_si_unit("length_name").should_be_called().will_return(False)
unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("SECOND") unit.is_si_unit("area_name").should_be_called().will_return(False)
unit.is_si_unit("volume_name").should_be_called().will_return(False)
ifc.run("unit.add_conversion_based_unit", name="foot").should_be_called().will_return("lengthunit") unit.is_si_unit("mass_name").should_be_called().will_return(False)
ifc.run("unit.add_conversion_based_unit", name="square foot").should_be_called().will_return("areaunit") unit.is_si_unit("time_name").should_be_called().will_return(False)
ifc.run("unit.add_conversion_based_unit", name="cubic foot").should_be_called().will_return("volumeunit") ifc.run("unit.add_conversion_based_unit", name="length_name").should_be_called().will_return("lengthunit")
ifc.run("unit.add_conversion_based_unit", name="pound").should_be_called().will_return("massunit") ifc.run("unit.add_conversion_based_unit", name="area_name").should_be_called().will_return("areaunit")
ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=None).should_be_called().will_return("timeunit") ifc.run("unit.add_conversion_based_unit", name="volume_name").should_be_called().will_return("volumeunit")
ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") ifc.run("unit.add_conversion_based_unit", name="mass_name").should_be_called().will_return("massunit")
ifc.run("unit.add_conversion_based_unit", name="time_name").should_be_called().will_return("timeunit")
ifc.run( ifc.run(
"unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "massunit", "timeunit"]
).should_be_called() ).should_be_called()
subject.assign_scene_units(ifc, unit) subject.assign_scene_units(ifc, unit)
def test_creating_and_assigning_imperial_units_without_mass_and_time(self, ifc, unit): def test_creating_both_metric_and_imperial_units(self, ifc, unit):
unit.is_scene_unit_metric().should_be_called().will_return(False) # I know British doctors measure with stones so...
unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("yard") unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("length_name")
unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square yard") unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("area_name")
unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic yard") unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return(None)
unit.add_mass_and_time_units().should_be_called().will_return(False) unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return(None)
ifc.run("unit.add_conversion_based_unit", name="yard").should_be_called().will_return("lengthunit") unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return(None)
ifc.run("unit.add_conversion_based_unit", name="square yard").should_be_called().will_return("areaunit") unit.is_si_unit("length_name").should_be_called().will_return(True)
ifc.run("unit.add_conversion_based_unit", name="cubic yard").should_be_called().will_return("volumeunit") unit.is_si_unit("area_name").should_be_called().will_return(False)
ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") unit.get_scene_unit_si_prefix("length_name").should_be_called().will_return("length_prefix")
ifc.run("unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit"]).should_be_called() ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="length_prefix").should_be_called().will_return(
subject.assign_scene_units(ifc, unit) "lengthunit"
)
def test_creating_metric_units_with_conversion_based_mass_and_time(self, ifc, unit): ifc.run("unit.add_conversion_based_unit", name="area_name").should_be_called().will_return("areaunit")
unit.is_scene_unit_metric().should_be_called().will_return(True) ifc.run("unit.assign_unit", units=["lengthunit", "areaunit"]).should_be_called()
unit.get_scene_unit_si_prefix("LENGTHUNIT").should_be_called().will_return("MILLI")
unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return(None)
unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return(None)
unit.add_mass_and_time_units().should_be_called().will_return(True)
unit.get_scene_unit_si_prefix("MASSUNIT").should_be_called().will_return("CONVERSION")
unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("tonne")
unit.get_scene_unit_si_prefix("TIMEUNIT").should_be_called().will_return("CONVERSION")
unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("minute")
ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="MILLI").should_be_called().will_return("lengthunit")
ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix=None).should_be_called().will_return("areaunit")
ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix=None).should_be_called().will_return("volumeunit")
ifc.run("unit.add_conversion_based_unit", name="tonne").should_be_called().will_return("massunit")
ifc.run("unit.add_conversion_based_unit", name="minute").should_be_called().will_return("timeunit")
ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit")
ifc.run(
"unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"]
).should_be_called()
subject.assign_scene_units(ifc, unit)
def test_creating_imperial_units_with_conversion_based_units(self, ifc, unit):
unit.is_scene_unit_metric().should_be_called().will_return(False)
unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("inch")
unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square inch")
unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic inch")
unit.add_mass_and_time_units().should_be_called().will_return(True)
unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("ounce")
unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("hour")
ifc.run("unit.add_conversion_based_unit", name="inch").should_be_called().will_return("lengthunit")
ifc.run("unit.add_conversion_based_unit", name="square inch").should_be_called().will_return("areaunit")
ifc.run("unit.add_conversion_based_unit", name="cubic inch").should_be_called().will_return("volumeunit")
ifc.run("unit.add_conversion_based_unit", name="ounce").should_be_called().will_return("massunit")
ifc.run("unit.add_conversion_based_unit", name="hour").should_be_called().will_return("timeunit")
ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit")
ifc.run(
"unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"]
).should_be_called()
subject.assign_scene_units(ifc, unit) subject.assign_scene_units(ifc, unit)
-12
View File
@@ -434,18 +434,6 @@ class TestImportUnits(NewFile):
assert second_prop.ifc_class == "IfcSIUnit" assert second_prop.ifc_class == "IfcSIUnit"
class TestIsSceneUnitMetric(NewFile):
def test_run(self):
assert bpy.context.scene
props = bpy.context.scene.unit_settings
props.system = "METRIC"
assert subject.is_scene_unit_metric() is True
props.system = "IMPERIAL"
assert subject.is_scene_unit_metric() is False
props.system = "NONE"
assert subject.is_scene_unit_metric() is True
class TestIsUnitClass: class TestIsUnitClass:
def test_run(self): def test_run(self):
ifc = ifcopenshell.file() ifc = ifcopenshell.file()
@@ -100,15 +100,10 @@ class Usecase:
size = self.convert_si_to_unit(1) size = self.convert_si_to_unit(1)
points = ((0.0, 0.0), (size, 0.0), (size, size), (0.0, size), (0.0, 0.0)) points = ((0.0, 0.0), (size, 0.0), (size, size), (0.0, size), (0.0, 0.0))
if self.polyline: if self.polyline:
# Only scale polyline if we have actual slope points = [
if self.x_angle and abs(self.x_angle) > 1e-6: (self.convert_si_to_unit(p[0]), self.convert_si_to_unit(p[1] * abs(1 / cos(self.x_angle))))
points = [ for p in self.polyline
(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": if self.file.schema == "IFC2X3":
curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in points]) curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in points])
else: else:
@@ -119,23 +114,21 @@ class Usecase:
else: else:
direction_ratios = (0.0, 0.0, 1.0) 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) 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)
# Calculate depth based on extrusion angle perpendicular_offset = self.convert_si_to_unit(self.offset) * abs(1 / cos(self.x_angle))
extrusion_angle = abs(self.x_angle) if self.x_angle else 0 perpendicular_depth = self.convert_si_to_unit(self.depth) * abs(1 / cos(self.x_angle))
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 position = None
# default position for IFC2X3 where .Position is not optional
if self.file.schema == "IFC2X3" or self.offset != 0: if self.file.schema == "IFC2X3" or self.offset != 0:
position_vector = ( position_vector = (
direction_ratios[0] * perpendicular_offset, offset_direction[0] * perpendicular_offset,
direction_ratios[1] * perpendicular_offset, offset_direction[1] * perpendicular_offset,
direction_ratios[2] * perpendicular_offset, offset_direction[2] * perpendicular_offset,
) )
position = self.file.createIfcAxis2Placement3D( position = self.file.createIfcAxis2Placement3D(
self.file.createIfcCartesianPoint(position_vector), self.file.createIfcCartesianPoint(position_vector),
@@ -85,6 +85,7 @@ class Usecase:
def create_item(self) -> ifcopenshell.entity_instance: def create_item(self) -> ifcopenshell.entity_instance:
length = self.convert_si_to_unit(self.settings["length"]) length = self.convert_si_to_unit(self.settings["length"])
thickness = self.convert_si_to_unit(self.settings["thickness"]) thickness = self.convert_si_to_unit(self.settings["thickness"])
thickness *= 1 / cos(self.settings["x_angle"])
if self.settings["direction_sense"] == "NEGATIVE": if self.settings["direction_sense"] == "NEGATIVE":
thickness *= -1 thickness *= -1
points = ( points = (
@@ -112,7 +113,7 @@ class Usecase:
self.file.createIfcDirection((1.0, 0.0, 0.0)), self.file.createIfcDirection((1.0, 0.0, 0.0)),
), ),
extrusion_direction, extrusion_direction,
self.convert_si_to_unit(self.settings["height"]), self.convert_si_to_unit(self.settings["height"]) * abs(1 / cos(self.settings["x_angle"])),
) )
if self.settings["booleans"]: if self.settings["booleans"]:
extrusion = self.apply_booleans(extrusion) extrusion = self.apply_booleans(extrusion)