Files
IfcOpenShell/src/bonsai/bonsai/bim/module/model/slab.py
T
Andrej730 a0b81e333e black .
2026-01-16 17:16:25 +05:00

1182 lines
48 KiB
Python

# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import json
import bmesh
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.geometry
import ifcopenshell.api.material
import ifcopenshell.api.pset
import ifcopenshell.api.type
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.unit
import ifcopenshell.util.type
import bonsai.core.type
import bonsai.core.geometry
import bonsai.core.root
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from math import cos, sin, pi, acos, degrees
from mathutils import Vector, Matrix
from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator
from bonsai.bim.module.model.polyline import PolylineOperator
from typing import Optional
class DumbSlabGenerator:
def __init__(self, relating_type: ifcopenshell.entity_instance):
self.relating_type = relating_type
def generate(self, insertion_type="CURSOR"):
self.file = tool.Ifc.get()
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
thicknesses = []
for rel in self.relating_type.HasAssociations:
if rel.is_a("IfcRelAssociatesMaterial"):
material = rel.RelatingMaterial
if material.is_a("IfcMaterialLayerSet"):
thicknesses = [l.LayerThickness for l in material.MaterialLayers]
break
if not sum(thicknesses):
return
self.body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
self.footprint_context = ifcopenshell.util.representation.get_context(
tool.Ifc.get(), "Plan", "FootPrint", "SKETCH_VIEW"
)
props = tool.Model.get_model_props()
self.polyline = None
self.container = None
self.container_obj = None
if container := tool.Root.get_default_container():
self.container = container
self.container_obj = tool.Ifc.get_object(container)
self.depth = sum(thicknesses) * self.unit_scale
self.width = 3
self.length = 3
self.rotation = 0
self.location = Vector((0, 0, 0))
self.x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else props.x_angle
if insertion_type == "POLYLINE":
return self.derive_from_polyline()
elif insertion_type == "WALLS":
return self.derive_from_walls()
elif insertion_type == "CURSOR":
return self.derive_from_cursor()
def derive_from_polyline(self):
polyline_props = tool.Model.get_polyline_props()
polyline_data = polyline_props.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
self.location = Vector((polyline_points[0].x, polyline_points[0].y, self.container_obj.location.z))
self.polyline = [tuple(Vector((p.x, p.y, 0.0)) - self.location) for p in polyline_points]
if len(self.polyline) <= 2:
return
# Always assume a closed polyline
if self.polyline[0] != self.polyline[-1]:
self.polyline.append(self.polyline[0])
return self.create_slab()
def derive_from_cursor(self):
self.location = bpy.context.scene.cursor.location
return self.create_slab()
def derive_from_walls(self):
walls = tool.Model.get_connected_walls(bpy.context.selected_objects)
polyline_points = []
poly = tool.Model.get_polygons_from_wall_axis(walls)
polyline_points = [tuple([v for v in c]) for c in poly.exterior.coords]
self.location = Vector((polyline_points[0][0], polyline_points[0][1], self.container_obj.location.z))
self.polyline = [tuple(Vector((p[0], p[1], 0.0)) - self.location) for p in polyline_points]
if len(self.polyline) <= 2:
return
# Always assume a closed polyline
if self.polyline[0] != self.polyline[-1]:
self.polyline.append(self.polyline[0])
return self.create_slab()
def create_slab(self):
ifc_classes = ifcopenshell.util.type.get_applicable_entities(self.relating_type.is_a(), self.file.schema)
# Standard cases are deprecated, so let's cull them
ifc_class = next(c for c in ifc_classes if "StandardCase" not in c)
mesh = bpy.data.meshes.new("Dummy")
obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(self.relating_type, ifc_class), mesh)
matrix_world = Matrix()
matrix_world.translation = self.location
if self.container_obj:
matrix_world.translation.z = self.container_obj.location.z
else:
matrix_world.translation.z
obj.matrix_world = matrix_world
bpy.context.view_layer.update()
element = bonsai.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class=ifc_class,
should_add_representation=False,
)
ifcopenshell.api.type.assign_type(self.file, related_objects=[element], relating_type=self.relating_type)
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
representation = ifcopenshell.api.geometry.add_slab_representation(
tool.Ifc.get(),
context=self.body_context,
depth=self.depth,
x_angle=self.x_angle,
polyline=self.polyline,
)
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,
)
obj.matrix_world = obj.matrix_world @ Matrix.Rotation(self.x_angle, 4, "X")
if self.footprint_context:
extrusion = tool.Model.get_extrusion(representation)
if extrusion.SweptArea.is_a("IfcArbitraryClosedProfileDef"):
curves = [extrusion.SweptArea.OuterCurve]
if extrusion.SweptArea.is_a("IfcArbitraryProfileDefWithVoids"):
curves.extend(extrusion.SweptArea.InnerCurves)
representation = ifcopenshell.api.geometry.add_footprint_representation(
tool.Ifc.get(),
context=self.footprint_context,
curves=curves,
)
ifcopenshell.api.geometry.assign_representation(
tool.Ifc.get(), product=element, 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.DumbLayer3"})
material = ifcopenshell.util.element.get_material(element)
material.LayerSetDirection = "AXIS3"
tool.Blender.select_object(obj)
return obj
class DumbSlabPlaner:
def regenerate_from_layer_set_usage(self, usecase_path, ifc_file, settings):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
obj = bpy.context.active_object
element = tool.Ifc.get_entity(obj)
# Called from material.add_layer or material.remove_layer
material = ifcopenshell.util.element.get_material(element)
material_set_usage = tool.Ifc.get().by_id(material.id())
if not getattr(material_set_usage, "ForLayerSet", False):
return
layer_set = material_set_usage.ForLayerSet
total_thickness = sum([l.LayerThickness for l in layer_set.MaterialLayers])
if not total_thickness:
return
self.change_thickness(element, total_thickness)
def regenerate_from_layer(self, layer: ifcopenshell.entity_instance) -> None:
for layer_set in layer.ToMaterialLayerSet:
self.regenerate_from_layer_set(layer_set)
def regenerate_from_layer_set(self, layer_set: ifcopenshell.entity_instance) -> None:
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
total_thickness = sum([l.LayerThickness for l in layer_set.MaterialLayers])
if not total_thickness:
return
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)
else:
for rel in inverse.AssociatedTo:
for element in rel.RelatedObjects:
self.change_thickness(element, total_thickness, preserve_offset=True)
def regenerate_from_type(self, usecase_path, ifc_file, settings):
relating_type = settings["relating_type"]
new_material = ifcopenshell.util.element.get_material(relating_type)
if not new_material or not new_material.is_a("IfcMaterialLayerSet"):
return
parametric = ifcopenshell.util.element.get_psets(settings["relating_type"]).get("EPset_Parametric")
layer_set_direction = None
if parametric:
layer_set_direction = parametric.get("LayerSetDirection", layer_set_direction)
new_thickness = sum([l.LayerThickness for l in new_material.MaterialLayers])
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
for related_object in settings["related_objects"]:
self._regenerate_from_type(related_object, layer_set_direction, new_thickness)
def _regenerate_from_type(
self, related_object: ifcopenshell.entity_instance, layer_set_direction: Optional[str], new_thickness: float
) -> None:
obj = tool.Ifc.get_object(related_object)
if not obj or not tool.Geometry.get_active_representation(obj):
return
material = ifcopenshell.util.element.get_material(related_object)
if not material or not material.is_a("IfcMaterialLayerSetUsage"):
return
if layer_set_direction:
material.LayerSetDirection = layer_set_direction
if material.LayerSetDirection == "AXIS3":
self.change_thickness(related_object, new_thickness)
def regenerate_from_occurence(self, element, material_set_usage):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
layer_set = material_set_usage.ForLayerSet
total_thickness = sum([l.LayerThickness for l in layer_set.MaterialLayers])
if not total_thickness:
return
self.change_thickness(element, total_thickness)
def change_thickness(
self, element: ifcopenshell.entity_instance, thickness: float, preserve_offset: bool = False
) -> None:
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")
obj = tool.Ifc.get_object(element)
if not obj:
return
if thickness == 0:
return
custom_offset = tool.Model.get_material_layer_custom_offset(element, obj)
layer_offset = (custom_offset * self.unit_scale) if custom_offset is not None else layer_params["offset"]
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if representation:
extrusion = tool.Model.get_extrusion(representation)
if extrusion:
direction_ratios = Vector(extrusion.ExtrudedDirection.DirectionRatios)
# 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
extrusion.Depth = perpendicular_depth
# Update position
ifc_position = extrusion.Position
if direction_ratios.length > 0:
offset_vector = direction_ratios.normalized() * perpendicular_offset
position = offset_vector
material = ifcopenshell.util.element.get_material(element)
if material and material.is_a("IfcMaterialLayerSetUsage"):
# 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
representation = ifcopenshell.api.geometry.add_slab_representation(
tool.Ifc.get(),
context=body_context,
depth=thickness * self.unit_scale,
x_angle=x_angle,
)
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)
class EnableEditingSketchExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_sketch_extrusion_profile"
bl_label = "Enable Editing Sketch Extrusion Profile"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
obj = context.active_object
element = tool.Ifc.get_entity(obj)
pset = ifcopenshell.util.element.get_psets(element).get("EPset_Parametric", None)
if pset and pset["Engine"] == "CADSketcher" and pset["Entities"]:
context.scene["sketcher"].update(json.loads(pset["Entities"]))
bpy.context.view_layer.update()
bpy.ops.wm.tool_set_by_id(name="sketcher.slvs_select")
bpy.ops.view3d.slvs_set_all_constraints_visibility(visibility="SHOW")
return {"FINISHED"}
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
extrusion = tool.Model.get_extrusion(body)
profile = extrusion.SweptArea
if extrusion.Position:
position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist())
position.matrix_world.translation *= self.unit_scale
else:
position = Matrix()
z_values = [v[2] for v in obj.bound_box]
element = tool.Ifc.get_entity(obj)
entities = context.scene.sketcher.entities
entities.ensure_origin_elements(context)
origin = entities.add_point_3d(obj.matrix_world @ Vector((0, 0, max(z_values))))
origin.fixed = True
origin.origin = True
normal = entities.add_normal_3d(obj.matrix_world.to_quaternion())
normal.fixed = True
normal.origin = True
wp = entities.add_workplane(origin, normal)
sketch = entities.add_sketch(wp)
self.vertices = []
self.edges = []
self.arcs = []
self.circles = []
profile = extrusion.SweptArea
if profile.is_a("IfcArbitraryClosedProfileDef"):
self.process_curve(obj, position, profile.OuterCurve)
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
for inner_curve in profile.InnerCurves:
self.process_curve(obj, position, inner_curve)
points = []
for vertex in self.vertices:
points.append(context.scene.sketcher.entities.add_point_2d(vertex.to_2d(), sketch))
for edge in self.edges:
context.scene.sketcher.entities.add_line_2d(points[edge[0]], points[edge[1]], sketch)
bpy.ops.view3d.slvs_set_active_sketch(index=sketch.slvs_index)
return {"FINISHED"}
def process_curve(self, obj, position, curve):
offset = len(self.vertices)
if curve.is_a("IfcPolyline"):
total_points = len(curve.Points)
last_index = len(curve.Points) - 1
for i, point in enumerate(curve.Points):
if i == last_index:
continue
global_point = position @ Vector(self.convert_unit_to_si(point.Coordinates)).to_3d()
self.vertices.append(global_point)
self.edges.extend([(i, i + 1) for i in range(offset, len(self.vertices))])
self.edges[-1] = (len(self.vertices) - 1, offset) # Close the loop
elif curve.is_a("IfcIndexedPolyCurve"):
is_arc = False
if curve.Segments:
for segment in curve.Segments:
if len(segment[0]) == 3: # IfcArcIndex
is_arc = True
local_point = self.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1])
global_point = position @ Vector(local_point).to_3d()
self.vertices.append(global_point)
local_point = self.convert_unit_to_si(curve.Points.CoordList[segment[0][1] - 1])
global_point = position @ Vector(local_point).to_3d()
self.vertices.append(global_point)
self.arcs.append([len(self.vertices) - 2, len(self.vertices) - 1])
else:
local_point = self.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1])
global_point = position @ Vector(local_point).to_3d()
self.vertices.append(global_point)
if is_arc:
self.arcs[-1].append(len(self.vertices) - 1)
is_arc = False
else:
for local_point in curve.Points.CoordList:
global_point = position @ Vector(self.convert_unit_to_si(local_point)).to_3d()
self.vertices.append(global_point)
# Curves without segments are self closing
del self.vertices[-1]
self.edges.extend([(i, i + 1) for i in range(offset, len(self.vertices))])
self.edges[-1] = (len(self.vertices) - 1, offset) # Close the loop
elif curve.is_a("IfcCircle"):
center = self.convert_unit_to_si(
Matrix(ifcopenshell.util.placement.get_axis2placement(curve.Position).tolist()).translation
)
radius = self.convert_unit_to_si(curve.Radius)
self.vertices.extend(
[
position @ Vector((center[0], center[1] - curve.Radius, 0.0)),
position @ Vector((center[0], center[1] + curve.Radius, 0.0)),
]
)
self.circles.append([offset, offset + 1])
self.edges.append((offset, offset + 1))
def convert_unit_to_si(self, value):
if isinstance(value, (tuple, list)):
return [v * self.unit_scale for v in value]
return value * self.unit_scale
class EditSketchExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_sketch_extrusion_profile"
bl_label = "Edit Sketch Extrusion Profile"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
extrusion = tool.Model.get_extrusion(representation)
if extrusion.Position:
position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist())
else:
position = Matrix()
position_i = position.inverted()
cad_sketcher = __import__("CAD_Sketcher-main")
sketch = context.scene.sketcher.active_sketch
converter = cad_sketcher.convertors.BezierConverter(context.scene, sketch)
converter.run()
profile = tool.Ifc.get().createIfcArbitraryClosedProfileDef("AREA")
for path in converter.paths:
points = []
lines = path[0]
last_index = len(lines) - 1
for i, line in enumerate(lines):
local_point = (position_i @ Vector(line.p1.co).to_3d()).to_2d()
points.append(tool.Ifc.get().createIfcCartesianPoint(local_point))
points.append(points[0])
curve = tool.Ifc.get().createIfcPolyline(points)
profile.OuterCurve = curve
old_profile = extrusion.SweptArea
extrusion.SweptArea = profile
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_profile)
pset = ifcopenshell.util.element.get_psets(element).get("EPset_Parametric", None)
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
else:
pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=element, name="EPset_Parametric")
ifcopenshell.api.pset.edit_pset(
tool.Ifc.get(),
pset=pset,
properties={"Engine": "CADSketcher", "Entities": json.dumps(context.scene["sketcher"].to_dict())},
)
bpy.ops.view3d.slvs_set_active_sketch(index=-1)
workplane = sketch.wp
p1 = workplane.p1
p1.origin = False
nm = workplane.nm
nm.origin = False
bpy.ops.view3d.slvs_delete_entity(index=sketch["slvs_index"])
bpy.ops.view3d.slvs_delete_entity(index=workplane["slvs_index"])
bpy.ops.view3d.slvs_delete_entity(index=p1["slvs_index"])
bpy.ops.view3d.slvs_delete_entity(index=nm["slvs_index"])
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
)
return {"FINISHED"}
class DisableEditingSketchExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_editing_sketch_extrusion_profile"
bl_label = "Disable Editing Sketch Extrusion Profile"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
sketch = context.scene.sketcher.active_sketch
bpy.ops.view3d.slvs_set_active_sketch(index=-1)
workplane = sketch.wp
p1 = workplane.p1
p1.origin = False
nm = workplane.nm
nm.origin = False
bpy.ops.view3d.slvs_delete_entity(index=sketch["slvs_index"])
bpy.ops.view3d.slvs_delete_entity(index=workplane["slvs_index"])
bpy.ops.view3d.slvs_delete_entity(index=p1["slvs_index"])
bpy.ops.view3d.slvs_delete_entity(index=nm["slvs_index"])
return {"FINISHED"}
def disable_editing_extrusion_profile(context):
ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT")
obj = context.active_object
element = tool.Ifc.get_entity(obj)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=body,
)
return {"FINISHED"}
class DisableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_editing_extrusion_profile"
bl_label = "Disable Editing Extrusion Profile"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
return disable_editing_extrusion_profile(context)
class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_extrusion_profile"
bl_label = "Enable Editing Extrusion Profile"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
obj = context.active_object
element = tool.Ifc.get_entity(obj)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
body = ifcopenshell.util.representation.resolve_representation(body)
extrusion = tool.Model.get_extrusion(body)
existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
layer_params = tool.Model.get_material_layer_parameters(element)
usage_type = tool.Model.get_usage_type(element)
if extrusion.Position:
position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist())
position.translation *= self.unit_scale
# Restore the position to before it was changed by the offset and x_angle
rot_matrix = Matrix.Rotation(existing_x_angle, 4, "X")
perpendicular_offset = layer_params["offset"] * abs(1 / cos(existing_x_angle))
offset_vector = Vector((0.0, 0.0, -perpendicular_offset))
rot_offset = offset_vector @ rot_matrix
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
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)
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"}
class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_extrusion_profile"
bl_label = "Edit Extrusion Profile"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT")
obj = context.active_object
element = tool.Ifc.get_entity(obj)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
body = ifcopenshell.util.representation.resolve_representation(body)
extrusion = tool.Model.get_extrusion(body)
existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
layer_params = tool.Model.get_material_layer_parameters(element)
usage_type = tool.Model.get_usage_type(element)
if extrusion.Position:
position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist())
position.translation *= self.unit_scale
# Restore the position to after it was changed by the offset and x_angle
rot_matrix = Matrix.Rotation(existing_x_angle, 4, "X")
perpendicular_offset = layer_params["offset"] * abs(1 / cos(existing_x_angle))
offset_vector = Vector((0.0, 0.0, -perpendicular_offset))
rot_offset = offset_vector @ rot_matrix
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
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)
if not profile:
def msg(self, context):
self.layout.label(text="INVALID PROFILE")
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
ProfileDecorator.install(
context, exit_edit_mode_callback=lambda: disable_editing_extrusion_profile(context)
)
bpy.ops.object.mode_set(mode="EDIT")
return
old_profile = extrusion.SweptArea
for inverse in tool.Ifc.get().get_inverse(old_profile):
ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile)
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_profile)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=body,
)
# Only certain classes should have a footprint
if element.is_a() not in ("IfcSlab", "IfcRamp"):
return
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
)
footprint_context = ifcopenshell.util.representation.get_context(
tool.Ifc.get(), "Plan", "FootPrint", "SKETCH_VIEW"
)
if not footprint_context:
return
curves = [profile.OuterCurve]
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
curves.extend(profile.InnerCurves)
new_footprint = ifcopenshell.api.geometry.add_footprint_representation(
tool.Ifc.get(), context=footprint_context, curves=curves
)
old_footprint = ifcopenshell.util.representation.get_representation(element, "Plan", "FootPrint", "SKETCH_VIEW")
if old_footprint:
for inverse in tool.Ifc.get().get_inverse(old_footprint):
ifcopenshell.util.element.replace_attribute(inverse, old_footprint, new_footprint)
bonsai.core.geometry.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_footprint)
else:
ifcopenshell.api.geometry.assign_representation(
tool.Ifc.get(), product=element, representation=new_footprint
)
class ResetVertex(bpy.types.Operator):
bl_idname = "bim.reset_vertex"
bl_label = "Reset Vertex"
bl_description = "Reset selected vertices group assignments (e.g. remove curve/circle)."
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
obj = context.active_object
return bool(obj) and obj.type == "MESH"
def cancel_message(self, msg):
self.report({"WARNING"}, msg)
return {"CANCELLED"}
def execute(self, context):
obj = context.active_object
bpy.ops.object.mode_set(mode="OBJECT")
selected_vertices = [v.index for v in obj.data.vertices if v.select]
for group in obj.vertex_groups:
group.remove(selected_vertices)
bpy.ops.object.mode_set(mode="EDIT")
return {"FINISHED"}
class SetArcIndex(bpy.types.Operator):
bl_idname = "bim.set_arc_index"
bl_label = "Set Arc Index"
bl_description = (
"Add an IfcArcIndex based 3 point arc for the selected vertices, add a vertex group to mark the created arc."
)
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
obj = context.active_object
return bool(obj) and obj.type == "MESH"
def cancel_message(self, msg):
self.report({"ERROR"}, msg)
return {"CANCELLED"}
def execute(self, context):
# NOTE: undo won't remove new verex group
# because of jumping between modes
obj = context.active_object
bm = tool.Blender.get_bmesh_for_mesh(obj.data)
selected_vertices = [v.index for v in bm.verts if v.select]
if len(selected_vertices) != 3:
return self.cancel_message("Select 3 vertices.")
bpy.ops.object.mode_set(mode="OBJECT")
group = obj.vertex_groups.new(name="IFCARCINDEX")
group.add(selected_vertices, 1, "REPLACE")
bpy.ops.object.mode_set(mode="EDIT")
return {"FINISHED"}
class AddSlabFromWall(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.draw_slab_from_wall"
bl_label = "Draw Slab From Wall"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.space_data.type == "VIEW_3D"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.relating_type = None
props = tool.Model.get_model_props()
relating_type_id = props.relating_type_id
if relating_type_id:
self.relating_type = tool.Ifc.get().by_id(int(relating_type_id))
def _execute(self, context):
if not self.relating_type:
return {"FINISHED"}
walls = tool.Model.get_connected_walls(bpy.context.selected_objects)
if not walls:
self.report(
{"WARNING"},
"Please select a closed loop of walls, or deselect the walls to add a slab using the polyline tool.",
)
return {"FINISHED"}
DumbSlabGenerator(self.relating_type).generate("WALLS")
return {"FINISHED"}
class DrawPolylineSlab(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
bl_idname = "bim.draw_polyline_slab"
bl_label = "Draw Polyline Slab"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.space_data.type == "VIEW_3D"
def __init__(self, *args, **kwargs):
bpy.types.Operator.__init__(self, *args, **kwargs)
PolylineOperator.__init__(self)
self.relating_type = None
props = tool.Model.get_model_props()
relating_type_id = props.relating_type_id
if relating_type_id:
self.relating_type = tool.Ifc.get().by_id(int(relating_type_id))
def create_slab_from_polyline(self, context):
if not self.relating_type:
return {"FINISHED"}
slab = DumbSlabGenerator(self.relating_type).generate("POLYLINE")
if not slab:
return
model_props = tool.Model.get_model_props()
direction_sense = model_props.direction_sense
offset = model_props.offset
model = tool.Ifc.get()
element = tool.Ifc.get_entity(slab)
material = ifcopenshell.util.element.get_material(element)
material_set_usage = model.by_id(material.id())
if not getattr(material_set_usage, "ForLayerSet", False):
return
attributes = {"OffsetFromReferenceLine": offset, "DirectionSense": direction_sense}
ifcopenshell.api.material.edit_layer_usage(
model,
usage=material_set_usage,
attributes=attributes,
)
DumbSlabPlaner().regenerate_from_occurence(element, material_set_usage)
def modal(self, context, event):
return IfcStore.execute_ifc_operator(self, context, event, method="MODAL")
def _modal(self, context, event):
if not self.relating_type:
self.report({"WARNING"}, "You need to select a slab type.")
PolylineDecorator.uninstall()
tool.Blender.update_viewport()
return {"FINISHED"}
PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
tool.Blender.update_viewport()
self.handle_lock_axis(context, event)
if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
self.handle_mouse_move(context, event)
return {"PASS_THROUGH"}
props = tool.Model.get_model_props()
if event.value == "RELEASE" and event.type == "F":
direction_sense = props.direction_sense
props.direction_sense = "NEGATIVE" if direction_sense == "POSITIVE" else "POSITIVE"
if event.value == "RELEASE" and event.type == "O":
items = ["TOP", "CENTER", "BOTTOM"]
index = items.index(props.offset_type_horizontal)
size = len(items)
props.offset_type_horizontal = items[((index + 1) % size)]
self.set_offset(context, self.relating_type)
custom_instructions = {"Choose Axis": {"icons": True, "keys": ["EVENT_X", "EVENT_Y"]}}
slab_config = [
f"Direction: {props.direction_sense}",
f"Offset Type: {props.offset_type_horizontal}",
f"Offset Value: {tool.Polyline.format_input_ui_units(props.offset * self.unit_scale)}",
]
self.handle_instructions(context, custom_instructions, slab_config)
self.handle_mouse_move(context, event, should_round=True)
self.choose_axis(event)
self.handle_snap_selection(context, event)
if (
not self.tool_state.is_input_on
and event.value == "RELEASE"
and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}
):
self.create_slab_from_polyline(context)
context.workspace.status_text_set(text=None)
ProductDecorator.uninstall()
PolylineDecorator.uninstall()
tool.Polyline.clear_polyline()
tool.Blender.update_viewport()
return {"FINISHED"}
self.handle_keyboard_input(context, event)
self.handle_inserting_polyline(context, event)
self.get_product_preview_data(context, self.relating_type)
cancel = self.handle_cancelation(context, event)
if cancel is not None:
ProductDecorator.uninstall()
return cancel
return {"RUNNING_MODAL"}
def invoke(self, context, event):
return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE")
def _invoke(self, context, event):
super().invoke(context, event)
ProductDecorator.install(context)
self.tool_state.use_default_container = True
self.tool_state.plane_method = "XY"
self.set_offset(context, self.relating_type)
return {"RUNNING_MODAL"}
class RecalculateSlab(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.recalculate_slab"
bl_label = "Recalculate Slab"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
walls = []
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if element.is_a("IfcSlab"):
for rel in element.ConnectedTo:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement.is_a("IfcWall"):
walls.append(tool.Ifc.get_object(rel.RelatedElement))
tool.Model.recalculate_walls(walls)
return {"FINISHED"}