Files
IfcOpenShell/src/bonsai/bonsai/bim/module/model/wall.py
T
Andrej730 210257de8e black .
2025-03-17 11:57:26 +05:00

1341 lines
59 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/>.
#
# pyright: reportUnnecessaryTypeIgnoreComment=error
import bpy
import copy
import math
import numpy as np
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.type
import mathutils.geometry
import bonsai.core.type
import bonsai.core.root
import bonsai.core.geometry
import bonsai.core.model as core
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from math import pi, sin, cos, degrees
from mathutils import Vector, Matrix
from bonsai.bim.module.model.opening import FilledOpeningGenerator
from bonsai.bim.module.model.decorator import PolylineDecorator, ProductDecorator
from bonsai.bim.module.model.polyline import PolylineOperator
from typing import Optional, assert_never, TYPE_CHECKING, get_args, Literal, Union, Any
class UnjoinWalls(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unjoin_walls"
bl_label = "Unjoin Walls"
bl_description = "Unjoin the selected walls"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
core.unjoin_walls(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model)
class ExtendWallsToUnderside(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.extend_walls_to_underside"
bl_label = "Extend Walls To Underside"
bl_description = "Extend and clip selected walls at the bottom faces of an object"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
slab = None
walls = []
if (obj := tool.Blender.get_active_object(is_selected=True)) and (element := tool.Ifc.get_entity(obj)):
slab = obj
for obj in tool.Blender.get_selected_objects(include_active=False):
if (element := tool.Ifc.get_entity(obj)) and tool.Model.get_usage_type(element) == "LAYER2":
walls.append(obj)
if slab and walls:
core.extend_wall_to_slab(tool.Ifc, tool.Geometry, tool.Model, slab, walls)
else:
self.report({"ERROR"}, "Please select at least one LAYER2 element and an active element")
class ExtendWallsToWall(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.extend_walls_to_wall"
bl_label = "Extend Walls To Wall"
bl_description = "Extend and trim selected walls to another wall"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
target_obj = None
objs = []
if (
(obj := tool.Blender.get_active_object(is_selected=True))
and (element := tool.Ifc.get_entity(obj))
and tool.Model.get_usage_type(element) == "LAYER2"
):
target_obj = obj
for obj in tool.Blender.get_selected_objects(include_active=False):
if (
obj != target_obj
and (element := tool.Ifc.get_entity(obj))
and tool.Model.get_usage_type(element) == "LAYER2"
):
objs.append(obj)
if target_obj and objs:
if tool.Ifc.is_moved(target_obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=target_obj)
joiner = DumbWallJoiner()
target_element = tool.Ifc.get_entity(target_obj)
for obj in objs:
if tool.Ifc.is_moved(obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
element = tool.Ifc.get_entity(obj)
ifcopenshell.api.geometry.connect_wall(
tool.Ifc.get(), wall1=element, wall2=target_element, is_atpath=True
)
joiner.recreate_wall(element, obj)
joiner.recreate_wall(target_element, target_obj)
else:
self.report({"ERROR"}, "Please select at least one LAYER2 element and one active LAYER2 element")
class AlignWall(bpy.types.Operator):
bl_idname = "bim.align_wall"
bl_label = "Align Wall"
bl_options = {"REGISTER", "UNDO"}
bl_description = """ Align the selected walls to the active wall:
'Ext.': align to the EXTERIOR face
'C/L': align to wall CENTERLINE
'Int.': align to the INTERIOR face"""
AlignType = Literal["CENTERLINE", "EXTERIOR", "INTERIOR"]
align_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
items=((i, i, "") for i in get_args(AlignType))
)
if TYPE_CHECKING:
align_type: AlignType
@classmethod
def poll(cls, context):
selected_valid_objects = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
return context.active_object and len(selected_valid_objects) > 1
def execute(self, context):
selected_objects = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
for obj in selected_objects:
if obj == context.active_object:
continue
aligner = DumbWallAligner(obj, context.active_object)
if self.align_type == "CENTERLINE":
aligner.align_centerline()
elif self.align_type == "EXTERIOR":
aligner.align_first_layer()
elif self.align_type == "INTERIOR":
aligner.align_last_layer()
else:
assert_never(self.align_type)
return {"FINISHED"}
class FlipWall(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.flip_wall"
bl_label = "Flip Wall"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Switch the origin from the min XY corner to the max XY corner, and rotates the origin by 180"
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
joiner = DumbWallJoiner()
for obj in selected_objs:
joiner.flip(obj)
return {"FINISHED"}
class SplitWall(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.split_wall"
bl_label = "Split Wall"
bl_options = {"REGISTER", "UNDO"}
bl_description = (
"Split selected wall into two walls in correspondence of Blender cursor. The cursor must be in the wall volume"
)
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
for obj in selected_objs:
DumbWallJoiner().split(obj, context.scene.cursor.location)
return {"FINISHED"}
class MergeWall(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.merge_wall"
bl_label = "Merge Wall"
bl_description = "Merge selected walls into one object"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
if len(selected_objs) == 2:
DumbWallJoiner().merge([o for o in selected_objs if o != context.active_object][0], context.active_object)
return {"FINISHED"}
class RecalculateWall(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.recalculate_wall"
bl_label = "Recalculate Wall"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
DumbWallRecalculator().recalculate(context.selected_objects)
return {"FINISHED"}
class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.change_extrusion_depth"
bl_label = "Update"
bl_description = "Update Height"
bl_options = {"REGISTER", "UNDO"}
depth: bpy.props.FloatProperty()
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
layer2_objs = []
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
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
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
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":
ifcopenshell.api.run(
"geometry.disconnect_element",
tool.Ifc.get(),
relating_element=rel.RelatingElement,
related_element=element,
)
layer2_objs.append(obj)
if layer2_objs:
DumbWallRecalculator().recalculate(layer2_objs)
return {"FINISHED"}
class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.change_extrusion_x_angle"
bl_label = "Update"
bl_description = "Update Angle"
bl_options = {"REGISTER", "UNDO"}
x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE")
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
layer2_objs = []
other_objs = []
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())
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
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
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
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))
print(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":
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
# Reset the transformation and returns to the original points with 0 degrees
extrusion.SweptArea.OuterCurve.Points.CoordList = [
(p[0], p[1] * abs(cos(existing_x_angle)))
for p in extrusion.SweptArea.OuterCurve.Points.CoordList
]
# Apply the transformation for the new x_angle
extrusion.SweptArea.OuterCurve.Points.CoordList = [
(p[0], p[1] * abs(1 / cos(x_angle))) for p in extrusion.SweptArea.OuterCurve.Points.CoordList
]
# 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 = direction_ratios.copy()
# Check angle and z direction to determine whether the extrusion direction is positive or negative
if (abs(x_angle) < (pi / 2) and direction_ratios.z > 0) or (
abs(x_angle) > (pi / 2) and direction_ratios.z < 0
):
# The extrusion direction is positive. If the layer_parameter is set to negative,
# then the we change the extrusion direction.
if layer_params["direction_sense"] == "NEGATIVE":
direction_ratios *= -1
elif ((x_angle) > (pi / 2) and direction_ratios.z > 0) or (
(x_angle) < (pi / 2) and direction_ratios.z < 0
):
# The extrusion direction is negative. If the layer_parameter is set to positive,
# then the we change the extrusion direction.
# then the we change the extrusion direction. And the offset direction should remain positive
# for either direction sense, so we change it.
offset_direction *= -1
if layer_params["direction_sense"] == "POSITIVE":
direction_ratios *= -1
extrusion.ExtrudedDirection.DirectionRatios = tuple(direction_ratios)
extrusion.Depth = perpendicular_depth
if extrusion.Position or perpendicular_offset != 0:
position = offset_direction * perpendicular_offset
tool.Model.add_extrusion_position(extrusion, position)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
# 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:
DumbWallRecalculator().recalculate(layer2_objs)
return {"FINISHED"}
class ChangeLayerLength(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.change_layer_length"
bl_label = "Update"
bl_description = "Update Length"
bl_options = {"REGISTER", "UNDO"}
length: bpy.props.FloatProperty()
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
joiner = DumbWallJoiner()
for obj in context.selected_objects:
joiner.set_length(obj, self.length)
return {"FINISHED"}
class AddWallsFromSlab(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.draw_walls_from_slab"
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):
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"}
slab = tool.Ifc.get_entity(context.active_object)
if not slab.is_a("IfcSlab"):
self.report(
{"WARNING"},
"Please select a slab.",
)
return {"FINISHED"}
walls = DumbWallGenerator(self.relating_type).generate("SLAB")
if walls:
for wall1, wall2 in zip(walls, walls[1:] + [walls[0]]):
DumbWallJoiner().connect(wall2["obj"], wall1["obj"])
class DrawPolylineWall(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
bl_idname = "bim.draw_polyline_wall"
bl_label = "Draw Polyline Wall"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.space_data.type == "VIEW_3D"
def __init__(self):
super().__init__()
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_walls_from_polyline(self, context: bpy.types.Context) -> Union[set[str], None]:
if not self.relating_type:
return {"FINISHED"}
model_props = tool.Model.get_model_props()
direction_sense = model_props.direction_sense
offset = model_props.offset
walls, is_polyline_closed = DumbWallGenerator(self.relating_type).generate("POLYLINE")
for wall in walls:
model = tool.Ifc.get()
element = tool.Ifc.get_entity(wall["obj"])
material = ifcopenshell.util.element.get_material(element)
material_set_usage = model.by_id(material.id())
# if material.is_a("IfcMaterialLayerSetUsage"):
attributes = {"OffsetFromReferenceLine": offset, "DirectionSense": direction_sense}
ifcopenshell.api.run(
"material.edit_layer_usage",
model,
**{"usage": material_set_usage, "attributes": attributes},
)
DumbWallRecalculator().recalculate([wall["obj"]])
if walls:
if is_polyline_closed:
for wall1, wall2 in zip(walls, walls[1:] + [walls[0]]):
DumbWallJoiner().connect(wall2["obj"], wall1["obj"])
else:
for wall1, wall2 in zip(walls[:-1], walls[1:]):
DumbWallJoiner().connect(wall2["obj"], wall1["obj"])
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 wall 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) # Must come before "PASS_TRHOUGH"
if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
self.handle_mouse_move(context, event)
return {"PASS_THROUGH"}
props = tool.Model.get_model_props()
# Wall axis settings
if event.value == "RELEASE" and event.type == "F":
direction_sense = props.direction_sense
props.direction_sense = "NEGATIVE" if direction_sense == "POSITIVE" else "POSITIVE"
self.set_offset(context, self.relating_type)
if event.value == "RELEASE" and event.type == "O":
items = ("EXTERIOR", "CENTER", "INTERIOR")
index = items.index(props.offset_type_vertical)
size = len(items)
props.offset_type_vertical = items[((index + 1) % size)]
self.set_offset(context, self.relating_type)
custom_instructions = {"Choose Axis": {"icons": True, "keys": ["EVENT_X", "EVENT_Y"]}}
wall_config = [
f"Direction: {props.direction_sense}",
f"Offset Type: {props.offset_type_vertical}",
f"Offset Value: {tool.Polyline.format_input_ui_units(props.offset * self.unit_scale)}",
]
self.handle_instructions(context, custom_instructions, wall_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_walls_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 DumbWallAligner:
# An alignment shifts the origin of all walls to the closest point on the
# local X axis of the reference wall. In addition, the Z rotation is copied.
# Z translations are ignored for alignment.
def __init__(self, wall: bpy.types.Object, reference_wall: bpy.types.Object):
self.wall = wall
self.reference_wall = reference_wall
def align_centerline(self) -> None:
self.align_rotation()
l_start = Vector(self.reference_wall.bound_box[0]).lerp(Vector(self.reference_wall.bound_box[3]), 0.5)
l_end = Vector(self.reference_wall.bound_box[4]).lerp(Vector(self.reference_wall.bound_box[7]), 0.5)
start = self.reference_wall.matrix_world @ l_start
end = self.reference_wall.matrix_world @ l_end
l_snap_point = Vector(self.wall.bound_box[0]).lerp(Vector(self.wall.bound_box[3]), 0.5)
snap_point = self.wall.matrix_world @ l_snap_point
offset = snap_point - self.wall.matrix_world.translation
point, _ = mathutils.geometry.intersect_point_line(snap_point, start, end)
new_origin = point - offset
self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy
def align_last_layer(self) -> None:
self.align_rotation()
if self.is_rotation_flipped():
element = tool.Ifc.get_entity(self.wall)
if tool.Model.get_usage_type(element) == "LAYER2":
DumbWallJoiner().flip(self.wall)
bpy.context.view_layer.update()
snap_point = self.wall.matrix_world @ Vector(self.wall.bound_box[3])
else:
snap_point = self.wall.matrix_world @ Vector(self.wall.bound_box[0])
else:
snap_point = self.wall.matrix_world @ Vector(self.wall.bound_box[3])
start = self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[3])
end = self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[7])
point, _ = mathutils.geometry.intersect_point_line(snap_point, start, end)
offset = snap_point - self.wall.matrix_world.translation
new_origin = point - offset
self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy
def align_first_layer(self) -> None:
self.align_rotation()
if self.is_rotation_flipped():
element = tool.Ifc.get_entity(self.wall)
if tool.Model.get_usage_type(element) == "LAYER2":
DumbWallJoiner().flip(self.wall)
bpy.context.view_layer.update()
snap_point = self.wall.matrix_world @ Vector(self.wall.bound_box[0])
else:
snap_point = self.wall.matrix_world @ Vector(self.wall.bound_box[3])
else:
snap_point = self.wall.matrix_world @ Vector(self.wall.bound_box[0])
start = self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[0])
end = self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[4])
point, _ = mathutils.geometry.intersect_point_line(snap_point, start, end)
offset = snap_point - self.wall.matrix_world.translation
new_origin = point - offset
self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy
def align_rotation(self) -> None:
reference = (self.reference_wall.matrix_world.to_quaternion() @ Vector((1, 0, 0))).to_2d()
wall = (self.wall.matrix_world.to_quaternion() @ Vector((1, 0, 0))).to_2d()
angle = reference.angle_signed(wall)
if round(degrees(angle) % 360) in (0, 180):
return
elif angle > (pi / 2):
self.wall.rotation_euler[2] -= pi - angle
else:
self.wall.rotation_euler[2] += angle
bpy.context.view_layer.update()
def is_rotation_flipped(self) -> bool:
reference = (self.reference_wall.matrix_world.to_quaternion() @ Vector((1, 0, 0))).to_2d()
wall = (self.wall.matrix_world.to_quaternion() @ Vector((1, 0, 0))).to_2d()
angle = reference.angle_signed(wall)
return round(degrees(angle) % 360) == 180
class DumbWallRecalculator:
def recalculate(self, walls: list[bpy.types.Object]) -> None:
queue: set[tuple[ifcopenshell.entity_instance, bpy.types.Object]] = set()
for wall in walls:
element = tool.Ifc.get_entity(wall)
if tool.Ifc.is_moved(wall):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall)
queue.add((element, wall))
for rel in getattr(element, "ConnectedTo", []):
obj = tool.Ifc.get_object(rel.RelatedElement)
if tool.Ifc.is_moved(obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
queue.add((rel.RelatedElement, obj))
for rel in getattr(element, "ConnectedFrom", []):
obj = tool.Ifc.get_object(rel.RelatingElement)
if tool.Ifc.is_moved(obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
queue.add((rel.RelatingElement, obj))
joiner = DumbWallJoiner()
for element, wall in queue:
if tool.Model.get_usage_type(element) == "LAYER2" and wall:
joiner.recreate_wall(element, wall)
class DumbWallGenerator:
def __init__(self, relating_type):
self.relating_type = relating_type
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
def generate(self, insertion_type="CURSOR"):
self.file = tool.Ifc.get()
self.layers = tool.Model.get_material_layer_parameters(self.relating_type)
if not self.layers["thickness"]:
return
self.body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
self.axis_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW")
props = tool.Model.get_model_props()
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.width = self.layers["thickness"]
self.height = props.extrusion_depth
self.length = props.length
self.rotation = 0.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 == "SLAB":
return self.derive_from_slab()
elif insertion_type == "CURSOR":
return self.derive_from_cursor()
def derive_from_polyline(self) -> tuple[list[Union[dict[str, Any], None]], bool]:
polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
is_polyline_closed = False
if len(polyline_points) > 3:
first_vec = Vector((polyline_points[0].x, polyline_points[0].y, polyline_points[0].z))
last_vec = Vector((polyline_points[-1].x, polyline_points[-1].y, polyline_points[-1].z))
if first_vec == last_vec:
is_polyline_closed = True
walls = []
for i in range(len(polyline_points) - 1):
vec1 = Vector((polyline_points[i].x, polyline_points[i].y, polyline_points[i].z))
vec2 = Vector((polyline_points[i + 1].x, polyline_points[i + 1].y, polyline_points[i + 1].z))
coords = (vec1, vec2)
walls.append(self.create_wall_from_2_points(coords))
return walls, is_polyline_closed
def derive_from_slab(self):
slab_obj = bpy.context.active_object
slab = tool.Ifc.get_entity(slab_obj)
container = ifcopenshell.util.element.get_container(slab)
self.container_obj = tool.Ifc.get_object(container)
elevation = self.container_obj.location.z
representation = ifcopenshell.util.representation.get_representation(slab, "Model", "Body", "MODEL_VIEW")
extrusion = tool.Model.get_extrusion(representation)
polyline_points = extrusion.SweptArea.OuterCurve.Points.CoordList
polyline_points = [[(v * self.unit_scale) for v in p] for p in polyline_points]
polyline_points = [slab_obj.matrix_world @ Vector((p[0], p[1], elevation)) for p in polyline_points]
if not tool.Cad.is_counter_clockwise_order(polyline_points[0], polyline_points[1], polyline_points[2]):
polyline_points = polyline_points[::-1]
walls = []
for i in range(len(polyline_points) - 1):
vec1 = polyline_points[i]
vec2 = polyline_points[i + 1]
coords = (vec1, vec2)
walls.append(self.create_wall_from_2_points(coords))
return walls
def create_wall_from_2_points(self, coords, should_round=False) -> Union[dict[str, Any], None]:
direction = coords[1] - coords[0]
length = direction.length
data = {"coords": coords}
self.length = length
self.rotation = math.atan2(direction[1], direction[0])
if should_round:
# Round to nearest 50mm (yes, metric for now)
self.length = 0.05 * round(length / 0.05)
# Round to nearest 5 degrees
nearest_degree = (math.pi / 180) * 5
self.rotation = nearest_degree * round(self.rotation / nearest_degree)
self.location = coords[0]
data["obj"] = self.create_wall()
return data
def derive_from_cursor(self) -> bpy.types.Object:
RAYCAST_PRECISION = 0.01
self.location = bpy.context.scene.cursor.location
if self.container:
for subelement in ifcopenshell.util.element.get_decomposition(self.container):
if not subelement.is_a("IfcWall"):
continue
sibling_obj = tool.Ifc.get_object(subelement)
if not sibling_obj or not isinstance(sibling_obj.data, bpy.types.Mesh):
continue
inv_obj_matrix = sibling_obj.matrix_world.inverted()
local_location = inv_obj_matrix @ self.location
try:
raycast = sibling_obj.closest_point_on_mesh(local_location, distance=RAYCAST_PRECISION)
except:
# If the mesh has no faces
raycast = [None]
if not raycast[0]:
continue
for face in sibling_obj.data.polygons:
normal = (sibling_obj.matrix_world.to_quaternion() @ face.normal).normalized()
face_center = sibling_obj.matrix_world @ face.center
if (
normal.z != 0
or abs(mathutils.geometry.distance_point_to_plane(self.location, face_center, normal)) > 0.01
):
continue
rotation = math.atan2(normal[1], normal[0])
rotated_y_axis = Matrix.Rotation(-rotation, 4, "Z")[1].xyz
# since wall thickness goes by local Y+ axis
# we find best position for the next wall
# by finding the face of another wall that will be very close to the some test point.
# test point is calculated by applying to cursor position some little offset along the face
#
# a bit different offset to be safe on raycast
test_pos = self.location + rotated_y_axis * RAYCAST_PRECISION * 1.1
test_pos_local = inv_obj_matrix @ test_pos
raycast = sibling_obj.closest_point_on_mesh(test_pos_local, distance=RAYCAST_PRECISION)
if not raycast[0]:
continue
self.rotation = rotation
break
if self.rotation != 0:
break
return self.create_wall()
def create_wall(self) -> bpy.types.Object:
props = tool.Model.get_model_props()
ifc_class = self.get_relating_type_class(self.relating_type)
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.Rotation(self.rotation, 4, "Z")
matrix_world.translation = self.location
if self.container_obj:
matrix_world.translation.z = self.container_obj.location.z + props.rl1
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.run("type.assign_type", self.file, related_objects=[element], relating_type=self.relating_type)
if self.axis_context:
representation = ifcopenshell.api.run(
"geometry.add_axis_representation",
tool.Ifc.get(),
context=self.axis_context,
axis=[(0.0, 0.0), (self.length, 0.0)],
)
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=representation
)
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
representation = ifcopenshell.api.run(
"geometry.add_wall_representation",
tool.Ifc.get(),
context=self.body_context,
thickness=self.layers["thickness"],
direction_sense=self.layers["direction_sense"],
offset=self.layers["offset"],
length=self.length,
height=self.height,
x_angle=self.x_angle,
)
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=representation
)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "Bonsai.DumbLayer2"})
material = ifcopenshell.util.element.get_material(element)
material.LayerSetDirection = "AXIS2"
obj.select_set(True)
return obj
def get_relating_type_class(self, relating_type):
classes = ifcopenshell.util.type.get_applicable_entities(relating_type.is_a(), tool.Ifc.get().schema)
return [c for c in classes if "StandardCase" not in c][0]
class DumbWallPlaner:
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:
walls = []
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 != "AXIS2":
continue
if tool.Ifc.get().schema == "IFC2X3":
for rel in tool.Ifc.get().get_inverse(inverse):
if not rel.is_a("IfcRelAssociatesMaterial"):
continue
walls.extend([tool.Ifc.get_object(e) for e in rel.RelatedObjects])
else:
for rel in inverse.AssociatedTo:
walls.extend([tool.Ifc.get_object(e) for e in rel.RelatedObjects])
DumbWallRecalculator().recalculate([w for w in set(walls) if w])
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(relating_type).get("EPset_Parametric")
layer_set_direction = None
if parametric:
layer_set_direction = parametric.get("LayerSetDirection", layer_set_direction)
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)
def _regenerate_from_type(
self, related_object: ifcopenshell.entity_instance, layer_set_direction: Optional[str]
) -> 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 == "AXIS2":
DumbWallRecalculator().recalculate([obj])
class DumbWallJoiner:
def __init__(self):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.axis_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW")
self.body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
def unjoin(self, wall1):
element1 = tool.Ifc.get_entity(wall1)
if not element1:
return
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type="ATSTART")
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type="ATEND")
axis1 = tool.Model.get_wall_axis(wall1)
axis = copy.deepcopy(axis1["reference"])
body = copy.deepcopy(axis1["reference"])
self.recreate_wall(element1, wall1, axis, body)
def split(self, wall1: bpy.types.Object, target: Vector) -> None:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
element1 = tool.Ifc.get_entity(wall1)
if not element1:
return
if tool.Ifc.is_moved(wall1):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1)
axis1 = tool.Model.get_wall_axis(wall1)
axis2 = copy.deepcopy(axis1)
intersect, cut_percentage = mathutils.geometry.intersect_point_line(target.to_2d(), *axis1["reference"])
if cut_percentage < 0 or cut_percentage > 1 or tool.Cad.is_x(cut_percentage, (0, 1)):
return
wall2 = self.duplicate_wall(wall1)
element2 = tool.Ifc.get_entity(wall2)
# Get the ATEND connection from wall1 to use it in wall2
relating_element = None
connections = element1.ConnectedTo
for conn in connections:
if conn.RelatingConnectionType == "ATEND":
relating_element = conn.RelatedElement
description = conn.Description
connections = element1.ConnectedFrom
for conn in connections:
if conn.RelatedConnectionType == "ATEND":
relating_element = conn.RelatingElement
description = conn.Description
if relating_element:
ifcopenshell.api.run(
"geometry.connect_path",
tool.Ifc.get(),
relating_element=relating_element,
related_element=element2,
relating_connection="ATSTART",
related_connection="ATEND",
description=description,
)
# During the duplication process, unfilled voids are copied, so we need
# to check openings on both element1 and element2. Let's check element1
# first.
for opening in [
r.RelatedOpeningElement for r in element1.HasOpenings if not r.RelatedOpeningElement.HasFillings
]:
opening_matrix = Matrix(ifcopenshell.util.placement.get_local_placement(opening.ObjectPlacement).tolist())
opening_matrix.translation *= unit_scale
opening_location = opening_matrix.translation
_, opening_position = mathutils.geometry.intersect_point_line(opening_location.to_2d(), *axis1["reference"])
if opening_position > cut_percentage:
# The opening should be removed from element1.
ifcopenshell.api.run("feature.remove_feature", tool.Ifc.get(), feature=opening)
# Now let's check element2.
for opening in [
r.RelatedOpeningElement for r in element2.HasOpenings if not r.RelatedOpeningElement.HasFillings
]:
opening_matrix = Matrix(ifcopenshell.util.placement.get_local_placement(opening.ObjectPlacement).tolist())
opening_matrix.translation *= unit_scale
opening_location = opening_matrix.translation
_, opening_position = mathutils.geometry.intersect_point_line(opening_location.to_2d(), *axis1["reference"])
if opening_position < cut_percentage:
# The opening should be removed from element2.
ifcopenshell.api.run("feature.remove_feature", tool.Ifc.get(), feature=opening)
# During the duplication process, filled voids are not copied. So we
# only need to check fillings on the original element1.
for opening in [r.RelatedOpeningElement for r in element1.HasOpenings if r.RelatedOpeningElement.HasFillings]:
filling_obj = tool.Ifc.get_object(opening.HasFillings[0].RelatedBuildingElement)
filling_location = filling_obj.matrix_world.translation
_, filling_position = mathutils.geometry.intersect_point_line(filling_location.to_2d(), *axis1["reference"])
if filling_position > cut_percentage:
# The filling should be moved from element1 to element2.
FilledOpeningGenerator().generate(filling_obj, wall2, target=filling_obj.matrix_world.translation)
p1, p2 = ifcopenshell.util.representation.get_reference_line(element1)
p3 = (wall1.matrix_world.inverted() @ intersect.to_3d()).to_2d() / unit_scale
self.set_axis(element1, p1, p3)
self.set_axis(element2, p3, p2)
self.recreate_wall(element1, wall1)
self.recreate_wall(element2, wall2)
def flip(self, wall1: bpy.types.Object) -> None:
if tool.Ifc.is_moved(wall1):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1)
if (
not (element1 := tool.Ifc.get_entity(wall1))
or not (usage := ifcopenshell.util.element.get_material(element1))
or not usage.is_a("IfcMaterialLayerSetUsage")
or usage.LayerSetDirection != "AXIS2"
):
return
thickness = sum([l.LayerThickness for l in usage.ForLayerSet.MaterialLayers])
if usage.DirectionSense == "POSITIVE":
usage.DirectionSense = "NEGATIVE"
else:
thickness *= -1
usage.DirectionSense = "POSITIVE"
matrix = ifcopenshell.util.placement.get_local_placement(element1.ObjectPlacement)
offset = matrix[:, 1] * thickness
matrix[:, 3] += offset
ifcopenshell.api.geometry.edit_object_placement(
tool.Ifc.get(), product=element1, matrix=matrix, is_si=False, should_transform_children=False
)
self.recreate_wall(element1, wall1)
def merge(self, wall1, wall2):
if tool.Ifc.is_moved(wall1):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1)
if tool.Ifc.is_moved(wall2):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall2)
element1 = tool.Ifc.get_entity(wall1)
element2 = tool.Ifc.get_entity(wall2)
p1, p2 = ifcopenshell.util.representation.get_reference_line(element1)
p3, p4 = ifcopenshell.util.representation.get_reference_line(element2)
matrix1i = np.linalg.inv(ifcopenshell.util.placement.get_local_placement(element1.ObjectPlacement))
matrix2 = ifcopenshell.util.placement.get_local_placement(element2.ObjectPlacement)
p3 = (matrix1i @ matrix2 @ np.concatenate((p3, (0, 1))))[:2]
p4 = (matrix1i @ matrix2 @ np.concatenate((p4, (0, 1))))[:2]
if not np.isclose(p1[1], p4[1]) or not np.isclose(p3[1], p4[1]):
return
x_ordinates = tuple(co[0] for co in (p1, p2, p3, p4))
p1[0] = min(x_ordinates)
p2[0] = max(x_ordinates)
self.set_axis(element1, p1, p2)
for rel in element2.ConnectedTo:
ifcopenshell.api.geometry.disconnect_path(
tool.Ifc.get(), element=element1, connection_type=rel.RelatingConnectionType
)
ifcopenshell.api.geometry.connect_path(
tool.Ifc.get(),
relating_element=element1,
related_element=rel.RelatedElement,
relating_connection=rel.RelatingConnectionType,
related_connection=rel.RelatedConnectionType,
)
for rel in element2.ConnectedFrom:
ifcopenshell.api.geometry.disconnect_path(
tool.Ifc.get(), element=element1, connection_type=rel.RelatedConnectionType
)
ifcopenshell.api.geometry.connect_path(
tool.Ifc.get(),
relating_element=rel.RelatingElement,
related_element=element1,
relating_connection=rel.RelatingConnectionType,
related_connection=rel.RelatedConnectionType,
)
self.recreate_wall(element1, wall1)
tool.Geometry.delete_ifc_object(wall2)
def duplicate_wall(self, wall1):
wall2 = wall1.copy()
wall2.data = wall2.data.copy()
for collection in wall1.users_collection:
collection.objects.link(wall2)
bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=wall2)
return wall2
def join_Z(self, wall1, slab2):
element1 = tool.Ifc.get_entity(wall1)
element2 = tool.Ifc.get_entity(slab2)
for rel in element1.ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements" and rel.Description == "TOP":
ifcopenshell.api.run(
"geometry.disconnect_element",
tool.Ifc.get(),
relating_element=rel.RelatingElement,
related_element=element1,
)
ifcopenshell.api.run(
"geometry.connect_element",
tool.Ifc.get(),
relating_element=element2,
related_element=element1,
description="TOP",
)
self.recreate_wall(element1, wall1)
def set_axis(self, wall, p1, p2):
axis = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW")
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
item = builder.polyline([p1, p2])
rep = builder.get_representation(axis, items=[item])
if old_rep := ifcopenshell.util.representation.get_representation(wall, axis):
ifcopenshell.util.element.replace_element(old_rep, rep)
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_rep)
else:
ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=rep)
def extend(self, wall1, target):
if tool.Ifc.is_moved(wall1):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1)
element1 = tool.Ifc.get_entity(wall1)
p1, p2 = ifcopenshell.util.representation.get_reference_line(element1)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
target = (wall1.matrix_world.inverted() @ target).to_2d() / unit_scale
intersect, connection = mathutils.geometry.intersect_point_line(target, p1, p2)
connection = "ATEND" if connection > 0.5 else "ATSTART"
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type=connection)
if connection == "ATEND":
self.set_axis(element1, p1, intersect)
else:
self.set_axis(element1, intersect, p2)
self.recreate_wall(element1, wall1)
def set_length(self, wall1, si_length):
element1 = tool.Ifc.get_entity(wall1)
if not element1:
return
if tool.Ifc.is_moved(wall1):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1)
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type="ATEND")
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
p1, p2 = ifcopenshell.util.representation.get_reference_line(element1)
p2[0] = p1[0] + si_length / unit_scale
self.set_axis(element1, p1, p2)
self.recreate_wall(element1, wall1)
def join_T(self, wall1, wall2):
element1 = tool.Ifc.get_entity(wall1)
element2 = tool.Ifc.get_entity(wall2)
axis1 = tool.Model.get_wall_axis(wall1)
axis2 = tool.Model.get_wall_axis(wall2)
intersect = tool.Cad.intersect_edges(axis1["reference"], axis2["reference"])
if intersect:
intersect, _ = intersect
else:
return
connection = "ATEND" if tool.Cad.edge_percent(intersect, axis1["reference"]) > 0.5 else "ATSTART"
ifcopenshell.api.run(
"geometry.connect_path",
tool.Ifc.get(),
related_element=element1,
relating_element=element2,
relating_connection="ATPATH",
related_connection=connection,
description="BUTT",
)
self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
def connect(self, obj1, obj2):
wall1 = tool.Ifc.get_entity(obj1)
wall2 = tool.Ifc.get_entity(obj2)
if tool.Ifc.is_moved(obj1):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj1)
if tool.Ifc.is_moved(obj2):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj2)
ifcopenshell.api.geometry.connect_wall(tool.Ifc.get(), wall1=wall1, wall2=wall2)
self.recreate_wall(wall1, obj1)
self.recreate_wall(wall2, obj2)
def recreate_wall(self, element: ifcopenshell.entity_instance, obj: bpy.types.Object, axis=None, body=None) -> None:
rep = ifcopenshell.api.geometry.regenerate_wall_representation(tool.Ifc.get(), element)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=rep,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
tool.Geometry.record_object_materials(obj)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
matrix = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
matrix[:, 3] *= unit_scale
obj.matrix_world = tool.Loader.apply_blender_offset_to_matrix_world(obj, matrix)
tool.Geometry.record_object_position(obj)
def create_matrix(self, p, x, y, z):
return Matrix([x, y, z, p]).to_4x4().transposed()
def get_extrusion_data(self, representation):
results = {"item": None, "height": 3.0, "x_angle": 0, "is_sloped": False, "direction": Vector((0, 0, 1))}
item = representation.Items[0]
while True:
if item.is_a("IfcExtrudedAreaSolid"):
results["item"] = item
x, y, z = item.ExtrudedDirection.DirectionRatios
if not tool.Cad.is_x(x, 0) or not tool.Cad.is_x(y, 0) or not tool.Cad.is_x(z, 1):
results["direction"] = Vector(item.ExtrudedDirection.DirectionRatios)
results["x_angle"] = Vector((0, 1)).angle_signed(Vector((y, z)))
results["is_sloped"] = True
results["height"] = (item.Depth * self.unit_scale) / abs(1 / cos(results["x_angle"]))
break
elif item.is_a("IfcBooleanClippingResult"): # should be before IfcBooleanResult check
item = item.FirstOperand
elif item.is_a("IfcBooleanResult"):
if item.FirstOperand.is_a("IfcExtrudedAreaSolid") or item.FirstOperand.is_a("IfcBooleanResult"):
item = item.FirstOperand
else:
item = item.SecondOperand
else:
break
return results
# TODO reimplement in new version and deprecate
def clip(self, wall1: bpy.types.Object, slab2: bpy.types.Object) -> float:
"""returns height of the clipped wall, adds clipping plane to `clippings`"""
element1 = tool.Ifc.get_entity(wall1)
element2 = tool.Ifc.get_entity(slab2)
assert element1 and element2
layers1 = tool.Model.get_material_layer_parameters(element1)
axis1 = tool.Model.get_wall_axis(wall1, layers1)
bases = [axis1["base"][0].to_3d(), axis1["base"][1].to_3d(), axis1["side"][0].to_3d(), axis1["side"][1].to_3d()]
bases = [Vector((v[0], v[1], wall1.matrix_world.translation.z)) for v in bases] # add wall Z location
representation = tool.Geometry.get_active_representation(wall1)
assert representation
extrusion = self.get_extrusion_data(representation)
wall_dir = wall1.matrix_world.to_quaternion() @ extrusion["direction"]
slab_element = tool.Ifc.get_entity(slab2)
slab_params = tool.Model.get_material_layer_parameters(slab_element)
slab_representation = ifcopenshell.util.representation.get_representation(
slab_element, "Model", "Body", "MODEL_VIEW"
)
assert slab_representation
slab_extrusion = tool.Model.get_extrusion(slab_representation)
existing_x_angle = tool.Model.get_existing_x_angle(slab_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
offset = slab_params["offset"]
if slab_params["direction_sense"] == "NEGATIVE":
offset -= slab_params["thickness"]
slab_pt = slab2.matrix_world @ Vector((0, 0, 0)) + Vector((0, 0, offset * abs(1 / cos(existing_x_angle))))
slab_dir = slab2.matrix_world.to_quaternion() @ Vector((0, 0, -1))
tops = [mathutils.geometry.intersect_line_plane(b, b + wall_dir, slab_pt, slab_dir) for b in bases]
top_index = max(range(4), key=lambda i: tops[i].z)
i_top = tops[top_index]
i_bottom = bases[top_index]
quaternion = slab2.matrix_world.to_quaternion()
x_axis = quaternion @ Vector((1, 0, 0))
y_axis = quaternion @ Vector((0, 1, 0))
z_axis = quaternion @ Vector((0, 0, 1))
self.clippings.append(
{
"type": "IfcBooleanClippingResult",
"operand_type": "IfcHalfSpaceSolid",
"matrix": self.create_matrix(i_top, x_axis, y_axis, z_axis),
}
)
return (i_top - i_bottom).length