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):
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_options = {"REGISTER", "UNDO"}
+86 -245
View File
@@ -40,11 +40,9 @@ import bonsai.core.root
import bonsai.core.type
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.model.decorator import (
PolylineDecorator,
ProductDecorator,
ProfileDecorator,
)
from math import cos, pi
from mathutils import Vector, Matrix
from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator
from bonsai.bim.module.model.polyline import PolylineOperator
@@ -231,17 +229,16 @@ class DumbSlabPlaner:
for inverse in tool.Ifc.get().get_inverse(layer_set):
if not inverse.is_a("IfcMaterialLayerSetUsage") or inverse.LayerSetDirection != "AXIS3":
continue
if tool.Ifc.get().schema == "IFC2X3":
for rel in tool.Ifc.get().get_inverse(inverse):
if not rel.is_a("IfcRelAssociatesMaterial"):
continue
for element in rel.RelatedObjects:
self.change_thickness(element, total_thickness, preserve_offset=True)
self.change_thickness(element, total_thickness)
else:
for rel in inverse.AssociatedTo:
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):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
@@ -251,12 +248,10 @@ class DumbSlabPlaner:
return
self.change_thickness(element, total_thickness)
def change_thickness(
self, element: ifcopenshell.entity_instance, thickness: float, preserve_offset: bool = False
) -> None:
def change_thickness(self, element: ifcopenshell.entity_instance, thickness: float) -> None:
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if tool.Model.get_usage_type(element) != "LAYER3":
return
layer_params = tool.Model.get_material_layer_parameters(element)
ifc_file = tool.Ifc.get()
body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
@@ -274,48 +269,72 @@ 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
# 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))
# 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))
else:
perpendicular_depth = thickness
perpendicular_offset = layer_offset
# 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
# Update 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:
offset_vector = direction_ratios.normalized() * perpendicular_offset
position = offset_vector
material = ifcopenshell.util.element.get_material(element)
if material and material.is_a("IfcMaterialLayerSetUsage"):
# Only set offset if not preserving it (preserves independent offsets per instance)
if not preserve_offset:
material.OffsetFromReferenceLine = position.z
if ifc_position:
ifc_position.Location.Coordinates = position
else:
tool.Model.add_extrusion_position(extrusion, position)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
)
else:
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
new_rep = ifcopenshell.api.geometry.add_slab_representation(
tool.Ifc.get(),
context=body_context,
depth=thickness * self.unit_scale,
x_angle=x_angle,
)
for inverse in tool.Ifc.get().get_inverse(representation):
ifcopenshell.util.element.replace_attribute(inverse, representation, new_rep)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=new_rep,
)
bonsai.core.geometry.remove_representation(
tool.Ifc, tool.Geometry, obj=obj, representation=representation
)
return
else:
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
@@ -328,118 +347,13 @@ class DumbSlabPlaner:
ifcopenshell.api.geometry.assign_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(
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)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
)
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)
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.
original_rotation_x = 0
if extrusion.Position:
@@ -732,49 +644,22 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
tranlation_matrix = Matrix.Translation(rot_offset)
position = position @ tranlation_matrix
# 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
# 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()
# 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)
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"}
@@ -796,7 +681,6 @@ 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())
@@ -810,38 +694,17 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
tranlation_matrix = Matrix.Translation(rot_offset)
position = position @ tranlation_matrix
# 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
# 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
else:
position = Matrix()
# 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)
profile = tool.Model.export_profile(obj, position=position, x_angle=existing_x_angle)
if not profile:
@@ -893,28 +756,6 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
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):
bl_idname = "bim.reset_vertex"
+47 -120
View File
@@ -48,6 +48,8 @@ import bonsai.core.root
import bonsai.core.type
import bonsai.tool as tool
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.opening import FilledOpeningGenerator
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")
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)))
# 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,
)
extrusion.Depth = self.depth / si_conversion * (1 / cos(x_angle))
if tool.Model.get_usage_type(element) == "LAYER2":
for rel in element.ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements":
related_element = rel.RelatedElement
if related_element.is_a() == "IfcWall":
layer2_objs.append(tool.Ifc.get_object(related_element))
ifcopenshell.api.geometry.disconnect_element(
ifc_file,
relating_element=rel.RelatingElement,
related_element=element,
)
layer2_objs.append(obj)
if layer2_objs:
tool.Model.recalculate_walls(layer2_objs)
return {"FINISHED"}
@@ -458,143 +442,81 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
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())
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)
if not element:
continue
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 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))
# Update extrusion direction
if local_direction_changed:
extrusion.ExtrudedDirection.DirectionRatios = tuple(new_local_direction)
# Always update depth
extrusion.Depth = perpendicular_depth
extrusion.ExtrudedDirection.DirectionRatios = (0.0, sin(x_angle), cos(x_angle))
layer2_objs.append(obj)
extrusion.Depth = perpendicular_depth
else:
if tool.Model.get_usage_type(element) == "LAYER3":
# 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
)
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
# 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
] # Reset the transformation and returns to the original points with 0 degrees
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)
# Calculate new extrusion direction with direction sense
base_local_direction = Vector((0.0, sin(x_angle), cos(x_angle)))
# 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)
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 = base_local_direction.copy()
offset_direction = direction_ratios.copy()
# 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
# 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
):
# 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":
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
direction_ratios *= -1
elif ((x_angle) > (pi / 2) and direction_ratios.z > 0) or (
(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
if layer_params["direction_sense"] == "POSITIVE":
final_local_direction *= -1
direction_ratios *= -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.ExtrudedDirection.DirectionRatios = tuple(direction_ratios)
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))
# 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(
tool.Ifc,
tool.Geometry,
@@ -602,6 +524,12 @@ 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"}
@@ -1096,7 +1024,6 @@ 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)
+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)
if obj := tool.Ifc.get_object(element):
tool.Root.set_object_name(obj, element)
if collection := tool.Blender.get_object_bim_props(obj).collection:
collection.name = f"{element.is_a()}/{element.Name or 'Unnamed'}"
tool.Collector.assign(obj)
bonsai.bim.handler.refresh_ui_data()
+5 -5
View File
@@ -951,11 +951,11 @@ class Spatial:
def get_active_container(cls): pass
def get_container(cls, element): pass
def get_decomposed_elements(cls, container, recursive): pass
def get_decomposition(cls, element): pass
def get_object_matrix(cls, 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_decomposition(cls, element): pass
def get_selected_product_types(cls): pass
def get_selected_products(cls): pass
def import_spatial_decomposition(cls): pass
def import_spatial_element(cls, element, level_index): pass
@@ -1125,15 +1125,15 @@ class Unit:
def disable_editing_units(cls): pass
def enable_editing_units(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_si_prefix(cls, name): pass
def import_unit_attributes(cls, unit): 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 set_active_unit(cls, unit): pass
def get_project_currency_unit(cls): pass
def get_currency_name(cls): pass
@interface
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
# 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):
+18 -147
View File
@@ -1024,7 +1024,7 @@ class Loader(bonsai.core.tool.Loader):
elif material.is_a("IfcMaterialLayerSetUsage"):
usage = material
layer_set = material.ForLayerSet
offset = usage.OffsetFromReferenceLine
offset = usage.OffsetFromReferenceLine * cls.unit_scale
sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1
elif material.is_a("IfcMaterialLayerSet"):
usage = None
@@ -1033,168 +1033,61 @@ class Loader(bonsai.core.tool.Loader):
sense_factor = 1
else:
return mesh
if len(layer_set.MaterialLayers) == 1:
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.from_mesh(mesh)
prev_co = None
advance_direction = None
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()
co = Vector((0.0, 0.0, offset))
advance_direction = no
elif usage.LayerSetDirection == "AXIS2":
# 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()
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
co = Vector((0.0, offset, 0.0))
no = cls.get_extrusion_vector(element).normalized()
no = no.cross(Vector([1.0, 0.0, 0.0]))
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])
# 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":
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
# 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
no *= sense_factor
# Cache this
body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
styles = {}
has_layer_styles = False
for i, material in enumerate(mesh.materials):
if style := tool.Ifc.get_entity(material):
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
for idx, (original_i, layer) in enumerate(layer_list):
if idx != last_i:
for i, layer in enumerate(layer_set.MaterialLayers):
if i != last_i:
prev_co = co.copy()
advance_vector = advance_direction * layer.LayerThickness * thickness_scale
co += advance_vector
co += no * 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 idx == last_i:
if i == last_i:
for face in bisect_geom["geom"]:
if isinstance(face, bmesh.types.BMFace):
center = face.calc_center_median()
if (center - co).dot(test_normal) >= 0:
if (center - co).dot(no) >= 0:
face.material_index = material_index
has_layer_styles = True
else:
for face in bisect_geom["geom"]:
if isinstance(face, bmesh.types.BMFace):
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
has_layer_styles = True
@@ -1207,35 +1100,13 @@ class Loader(bonsai.core.tool.Loader):
return mesh
@classmethod
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"):
def get_extrusion_vector(cls, wall):
if body := ifcopenshell.util.representation.get_representation(wall, "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)
# 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(item.ExtrudedDirection.DirectionRatios)
return Vector([0.0, 0.0, 1.0])
@classmethod
-5
View File
@@ -419,11 +419,6 @@ class Unit(bonsai.core.tool.Unit):
new.is_assigned = unit in assigned_units
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
def is_unit_class(cls, unit: ifcopenshell.entity_instance, ifc_class: str) -> bool:
return unit.is_a(ifc_class)
+1 -1
View File
@@ -193,7 +193,7 @@ def main() -> None:
print(f"Downloading {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__":
@@ -74,6 +74,50 @@ Scenario: Assign container
When I click "CHECKMARK"
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
Given an empty IFC project
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" 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
And I add a cube
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"
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
Given an empty IFC project
And I add a cube
+70 -98
View File
@@ -22,119 +22,91 @@ from test.core.bootstrap import ifc, unit
class TestAssignSceneUnits:
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_si_prefix("LENGTHUNIT").should_be_called().will_return("prefix")
unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return("prefix")
unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return("prefix")
unit.add_mass_and_time_units().should_be_called().will_return(True)
unit.get_scene_unit_si_prefix("MASSUNIT").should_be_called().will_return("KILO")
unit.get_scene_unit_si_prefix("TIMEUNIT").should_be_called().will_return(None)
ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="prefix").should_be_called().will_return(
unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("length_name")
unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("area_name")
unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("volume_name")
unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("mass_name")
unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("time_name")
unit.is_si_unit("length_name").should_be_called().will_return(True)
unit.is_si_unit("area_name").should_be_called().will_return(True)
unit.is_si_unit("volume_name").should_be_called().will_return(True)
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"
)
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="VOLUMEUNIT", prefix="prefix").should_be_called().will_return(
ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix="area_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"
)
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="TIMEUNIT", prefix=None).should_be_called().will_return("timeunit")
ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit")
ifc.run("unit.add_si_unit", unit_type="MASSUNIT", prefix="mass_prefix").should_be_called().will_return(
"massunit"
)
ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix="time_prefix").should_be_called().will_return(
"timeunit"
)
ifc.run(
"unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"]
"unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "massunit", "timeunit"]
).should_be_called()
subject.assign_scene_units(ifc, unit)
def test_creating_and_assigning_metric_units_without_mass_and_time(self, ifc, unit):
unit.is_scene_unit_metric().should_be_called().will_return(True)
unit.get_scene_unit_si_prefix("LENGTHUNIT").should_be_called().will_return("CENTI")
unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return("CENTI")
unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return("CENTI")
unit.add_mass_and_time_units().should_be_called().will_return(False)
ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="CENTI").should_be_called().will_return("lengthunit")
ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix="CENTI").should_be_called().will_return("areaunit")
ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix="CENTI").should_be_called().will_return("volumeunit")
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"]).should_be_called()
def test_creating_and_assigning_only_specified_units(self, ifc, unit):
unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("length_name")
unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return(None)
unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return(None)
unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return(None)
unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return(None)
unit.is_si_unit("length_name").should_be_called().will_return(True)
unit.get_scene_unit_si_prefix("length_name").should_be_called().will_return("length_prefix")
ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="length_prefix").should_be_called().will_return(
"lengthunit"
)
ifc.run("unit.assign_unit", units=["lengthunit"]).should_be_called()
subject.assign_scene_units(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("foot")
unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square foot")
unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic foot")
unit.add_mass_and_time_units().should_be_called().will_return(True)
unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("pound")
unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("SECOND")
ifc.run("unit.add_conversion_based_unit", name="foot").should_be_called().will_return("lengthunit")
ifc.run("unit.add_conversion_based_unit", name="square foot").should_be_called().will_return("areaunit")
ifc.run("unit.add_conversion_based_unit", name="cubic foot").should_be_called().will_return("volumeunit")
ifc.run("unit.add_conversion_based_unit", name="pound").should_be_called().will_return("massunit")
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="degree").should_be_called().will_return("planeangleunit")
unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("length_name")
unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("area_name")
unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("volume_name")
unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("mass_name")
unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("time_name")
unit.is_si_unit("length_name").should_be_called().will_return(False)
unit.is_si_unit("area_name").should_be_called().will_return(False)
unit.is_si_unit("volume_name").should_be_called().will_return(False)
unit.is_si_unit("mass_name").should_be_called().will_return(False)
unit.is_si_unit("time_name").should_be_called().will_return(False)
ifc.run("unit.add_conversion_based_unit", name="length_name").should_be_called().will_return("lengthunit")
ifc.run("unit.add_conversion_based_unit", name="area_name").should_be_called().will_return("areaunit")
ifc.run("unit.add_conversion_based_unit", name="volume_name").should_be_called().will_return("volumeunit")
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(
"unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"]
"unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "massunit", "timeunit"]
).should_be_called()
subject.assign_scene_units(ifc, unit)
def test_creating_and_assigning_imperial_units_without_mass_and_time(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("yard")
unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square yard")
unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic yard")
unit.add_mass_and_time_units().should_be_called().will_return(False)
ifc.run("unit.add_conversion_based_unit", name="yard").should_be_called().will_return("lengthunit")
ifc.run("unit.add_conversion_based_unit", name="square yard").should_be_called().will_return("areaunit")
ifc.run("unit.add_conversion_based_unit", name="cubic yard").should_be_called().will_return("volumeunit")
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"]).should_be_called()
subject.assign_scene_units(ifc, unit)
def test_creating_metric_units_with_conversion_based_mass_and_time(self, ifc, unit):
unit.is_scene_unit_metric().should_be_called().will_return(True)
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()
def test_creating_both_metric_and_imperial_units(self, ifc, unit):
# I know British doctors measure with stones so...
unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("length_name")
unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("area_name")
unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return(None)
unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return(None)
unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return(None)
unit.is_si_unit("length_name").should_be_called().will_return(True)
unit.is_si_unit("area_name").should_be_called().will_return(False)
unit.get_scene_unit_si_prefix("length_name").should_be_called().will_return("length_prefix")
ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="length_prefix").should_be_called().will_return(
"lengthunit"
)
ifc.run("unit.add_conversion_based_unit", name="area_name").should_be_called().will_return("areaunit")
ifc.run("unit.assign_unit", units=["lengthunit", "areaunit"]).should_be_called()
subject.assign_scene_units(ifc, unit)
-12
View File
@@ -434,18 +434,6 @@ class TestImportUnits(NewFile):
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:
def test_run(self):
ifc = ifcopenshell.file()
@@ -100,15 +100,10 @@ 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:
# 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]
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
]
if self.file.schema == "IFC2X3":
curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in points])
else:
@@ -119,23 +114,21 @@ class Usecase:
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)
# 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)
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))
position = None
# default position for IFC2X3 where .Position is not optional
if self.file.schema == "IFC2X3" or self.offset != 0:
position_vector = (
direction_ratios[0] * perpendicular_offset,
direction_ratios[1] * perpendicular_offset,
direction_ratios[2] * perpendicular_offset,
offset_direction[0] * perpendicular_offset,
offset_direction[1] * perpendicular_offset,
offset_direction[2] * perpendicular_offset,
)
position = self.file.createIfcAxis2Placement3D(
self.file.createIfcCartesianPoint(position_vector),
@@ -85,6 +85,7 @@ 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 = (
@@ -112,7 +113,7 @@ class Usecase:
self.file.createIfcDirection((1.0, 0.0, 0.0)),
),
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"]:
extrusion = self.apply_booleans(extrusion)