mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-19 14:41:25 +00:00
1341 lines
59 KiB
Python
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
|