Files
IfcOpenShell/src/blenderbim/blenderbim/bim/module/model/profile.py
T
2023-01-09 21:55:22 +11:00

915 lines
41 KiB
Python

# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on 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.
#
# BlenderBIM Add-on 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 BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import copy
import math
import bmesh
import mathutils.geometry
import ifcopenshell
import ifcopenshell.util.type
import ifcopenshell.util.unit
import ifcopenshell.util.element
import blenderbim.bim.handler
import blenderbim.tool as tool
import blenderbim.core.type
import blenderbim.core.geometry
from blenderbim.bim.ifc import IfcStore
from math import pi, degrees, inf
from mathutils import Vector, Matrix
from ifcopenshell.api.pset.data import Data as PsetData
from ifcopenshell.api.material.data import Data as MaterialData
from blenderbim.bim.module.geometry.helper import Helper
def element_listener(element, obj):
blenderbim.bim.handler.subscribe_to(obj, "mode", mode_callback)
def mode_callback(obj, data):
for obj in set(bpy.context.selected_objects + [bpy.context.active_object]):
if (
not obj.data
or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve))
or not obj.BIMObjectProperties.ifc_definition_id
or not bpy.context.scene.BIMProjectProperties.is_authoring
):
return
product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbProfile":
return
if obj.mode == "EDIT":
IfcStore.edited_objs.add(obj)
bm = bmesh.from_edit_mesh(obj.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.update_edit_mesh(obj.data)
bm.free()
else:
material_usage = ifcopenshell.util.element.get_material(product)
x, y = obj.dimensions[0:2]
if not material_usage.CardinalPoint:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, y, 0)) / 2))
elif material_usage.CardinalPoint == 1:
new_origin = obj.matrix_world @ Vector(obj.bound_box[4])
elif material_usage.CardinalPoint == 2:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, 0, 0)) / 2))
elif material_usage.CardinalPoint == 3:
new_origin = obj.matrix_world @ Vector(obj.bound_box[0])
elif material_usage.CardinalPoint == 4:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[4]) + (Vector((0, y, 0)) / 2))
elif material_usage.CardinalPoint == 5:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, y, 0)) / 2))
elif material_usage.CardinalPoint == 6:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((0, y, 0)) / 2))
elif material_usage.CardinalPoint == 7:
new_origin = obj.matrix_world @ Vector(obj.bound_box[7])
elif material_usage.CardinalPoint == 8:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[3]) + (Vector((x, 0, 0)) / 2))
elif material_usage.CardinalPoint == 9:
new_origin = obj.matrix_world @ Vector(obj.bound_box[3])
if (obj.matrix_world.translation - new_origin).length < 0.001:
return
obj.data.transform(
Matrix.Translation(
(obj.matrix_world.inverted().to_quaternion() @ (obj.matrix_world.translation - new_origin))
)
)
obj.matrix_world.translation = new_origin
class DumbProfileGenerator:
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, link_to_scene=True):
self.file = IfcStore.get_file()
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
material = ifcopenshell.util.element.get_material(self.relating_type)
if material and material.is_a("IfcMaterialProfileSet"):
self.profile_set = material
else:
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(), "Model", "Axis", "GRAPH_VIEW")
props = bpy.context.scene.BIMModelProperties
self.collection = bpy.context.view_layer.active_layer_collection.collection
self.collection_obj = bpy.data.objects.get(self.collection.name)
self.depth = props.extrusion_depth * self.unit_scale
self.rotation = 0
self.location = Vector((0, 0, 0))
self.cardinal_point = int(bpy.context.scene.BIMModelProperties.cardinal_point)
return self.derive_from_cursor(link_to_scene=link_to_scene)
def derive_from_cursor(self, link_to_scene):
self.location = bpy.context.scene.cursor.location
return self.create_profile(link_to_scene)
def create_profile(self, link_to_scene):
ifc_classes = ifcopenshell.util.type.get_applicable_entities(self.relating_type.is_a(), self.file.schema)
# Standard cases are deprecated, so let's cull them
ifc_class = [c for c in ifc_classes if "StandardCase" not in c][0]
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()
if self.relating_type.is_a() in ["IfcBeamType", "IfcMemberType"]:
matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world
matrix_world.col[3] = self.location.to_4d()
if link_to_scene and self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
matrix_world[2][3] = self.collection_obj.location[2]
if link_to_scene:
self.collection.objects.link(obj)
element = blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class=ifc_class,
should_add_representation=False,
context=self.body_context,
)
ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=self.relating_type)
material = ifcopenshell.util.element.get_material(element)
material.CardinalPoint = self.cardinal_point
obj.matrix_world = matrix_world
bpy.context.view_layer.update()
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
if self.axis_context:
representation = ifcopenshell.api.run(
"geometry.add_axis_representation",
tool.Ifc.get(),
context=self.axis_context,
axis=[(0.0, 0.0, 0.0), (0.0, 0.0, self.depth)],
)
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=representation
)
representation = ifcopenshell.api.run(
"geometry.add_profile_representation",
tool.Ifc.get(),
context=self.body_context,
profile=self.profile_set.CompositeProfile or self.profile_set.MaterialProfiles[0].Profile,
cardinal_point=self.cardinal_point,
depth=self.depth,
)
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=representation
)
blenderbim.core.geometry.switch_representation(
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": "BlenderBIM.DumbProfile"})
MaterialData.load(self.file)
if link_to_scene:
obj.select_set(True)
return obj
class DumbProfileRegenerator:
def regenerate_from_profile_def(self, profile):
self.file = tool.Ifc.get()
objs = []
if not profile:
return
for element in self.get_elements_using_profile(profile):
obj = tool.Ifc.get_object(element)
if obj:
objs.append(obj)
DumbProfileRecalculator().recalculate(objs)
def regenerate_from_profile(self, usecase_path, ifc_file, settings):
self.file = ifc_file
objs = []
profile = settings["profile"].Profile
if not profile:
return
for element in self.get_elements_using_profile(profile):
obj = tool.Ifc.get_object(element)
if obj:
objs.append(obj)
DumbProfileRecalculator().recalculate(objs)
def get_elements_using_profile(self, profile):
results = []
for profile_set in [
mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile")
]:
for inverse in self.file.get_inverse(profile_set):
if not inverse.is_a("IfcMaterialProfileSetUsage"):
continue
if self.file.schema == "IFC2X3":
for rel in self.file.get_inverse(inverse):
if not rel.is_a("IfcRelAssociatesMaterial"):
continue
results.extend(rel.RelatedObjects)
else:
for rel in inverse.AssociatedTo:
results.extend(rel.RelatedObjects)
return results
def regenerate_from_type(self, usecase_path, ifc_file, settings):
obj = tool.Ifc.get_object(settings["related_object"])
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
return
new_material = ifcopenshell.util.element.get_material(settings["relating_type"])
if not new_material or not new_material.is_a("IfcMaterialProfileSet"):
return
DumbProfileRecalculator().recalculate([obj])
class ExtendProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.extend_profile"
bl_label = "Extend Profile"
bl_options = {"REGISTER", "UNDO"}
join_type: bpy.props.StringProperty()
def _execute(self, context):
selected_objs = context.selected_objects
joiner = DumbProfileJoiner()
if not self.join_type:
for obj in selected_objs:
joiner.unjoin(obj)
return {"FINISHED"}
if not context.active_object:
return {"FINISHED"}
if len(selected_objs) == 1:
joiner.join_E(context.active_object, context.scene.cursor.location)
return {"FINISHED"}
if len(selected_objs) == 2:
if self.join_type == "L":
joiner.join_L([o for o in selected_objs if o != context.active_object][0], context.active_object)
elif self.join_type == "V":
joiner.join_V([o for o in selected_objs if o != context.active_object][0], context.active_object)
if len(selected_objs) < 2:
return {"FINISHED"}
if self.join_type == "T":
for obj in selected_objs:
if obj == context.active_object:
continue
joiner.join_T(obj, context.active_object)
return {"FINISHED"}
class DumbProfileJoiner:
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(), "Model", "Axis", "GRAPH_VIEW")
self.body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
def unjoin(self, profile1):
element1 = tool.Ifc.get_entity(profile1)
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 = self.get_profile_axis(profile1)
axis = copy.deepcopy(axis1)
body = copy.deepcopy(axis1)
self.recreate_profile(element1, profile1, axis, body)
def join_E(self, profile1, target):
element1 = tool.Ifc.get_entity(profile1)
if not element1:
return
axis1 = self.get_profile_axis(profile1)
intersect, connection = mathutils.geometry.intersect_point_line(target, *axis1)
connection = "ATEND" if connection > 0.5 else "ATSTART"
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type=connection)
axis = copy.deepcopy(axis1)
body = copy.deepcopy(axis1)
axis[1 if connection == "ATEND" else 0] = intersect
body[1 if connection == "ATEND" else 0] = intersect
self.recreate_profile(element1, profile1, axis, body)
def set_depth(self, profile1, length):
element1 = tool.Ifc.get_entity(profile1)
if not element1:
return
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type="ATEND")
axis1 = self.get_profile_axis(profile1)
axis = copy.deepcopy(axis1)
body = copy.deepcopy(axis1)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
si_length = unit_scale * length
end = profile1.matrix_world @ Vector((si_length, 0, 0))
axis[1] = end
body[1] = end
self.recreate_profile(element1, profile1, axis, body)
def join_T(self, profile1, profile2):
element1 = tool.Ifc.get_entity(profile1)
element2 = tool.Ifc.get_entity(profile2)
axis1 = self.get_profile_axis(profile1)
axis2 = self.get_profile_axis(profile2)
intersect = tool.Cad.intersect_edges(axis1, axis2)
if intersect:
intersect, _ = intersect
else:
return
connection = "ATEND" if tool.Cad.edge_percent(intersect, axis1) > 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_profile(element1, profile1, axis1, axis1)
def join_V(self, profile1, profile2):
element1 = tool.Ifc.get_entity(profile1)
element2 = tool.Ifc.get_entity(profile2)
axis1 = self.get_profile_axis(profile1)
axis2 = self.get_profile_axis(profile2)
intersect = tool.Cad.intersect_edges(axis1, axis2)
if intersect:
intersect, _ = intersect
else:
return
profile1_end = "ATEND" if tool.Cad.edge_percent(intersect, axis1) > 0.5 else "ATSTART"
profile2_end = "ATEND" if tool.Cad.edge_percent(intersect, axis2) > 0.5 else "ATSTART"
ifcopenshell.api.run(
"geometry.connect_path",
tool.Ifc.get(),
relating_element=element1,
related_element=element2,
relating_connection=profile1_end,
related_connection=profile2_end,
description="MITRE",
)
self.recreate_profile(element1, profile1, axis1, axis1)
self.recreate_profile(element2, profile2, axis2, axis2)
def join_L(self, profile1, profile2):
element1 = tool.Ifc.get_entity(profile1)
element2 = tool.Ifc.get_entity(profile2)
axis1 = self.get_profile_axis(profile1)
axis2 = self.get_profile_axis(profile2)
intersect = tool.Cad.intersect_edges(axis1, axis2)
if intersect:
intersect, _ = intersect
else:
return
profile1_end = "ATEND" if tool.Cad.edge_percent(intersect, axis1) > 0.5 else "ATSTART"
profile2_end = "ATEND" if tool.Cad.edge_percent(intersect, axis2) > 0.5 else "ATSTART"
ifcopenshell.api.run(
"geometry.connect_path",
tool.Ifc.get(),
relating_element=element1,
related_element=element2,
relating_connection=profile1_end,
related_connection=profile2_end,
description="BUTT",
)
self.recreate_profile(element1, profile1, axis1, axis1)
self.recreate_profile(element2, profile2, axis2, axis2)
def recreate_profile(self, element, obj, axis=None, body=None):
if axis is None or body is None:
axis = body = self.get_profile_axis(obj)
self.axis = copy.deepcopy(axis)
self.body = copy.deepcopy(body)
material = ifcopenshell.util.element.get_material(element, should_skip_usage=False)
usage = None
if not material or not material.is_a("IfcMaterialProfileSet"):
return
if material.is_a("IfcMaterialProfileSetUsage"):
usage = material
material = material.ForProfileSet
self.profile = material.CompositeProfile or material.MaterialProfiles[0].Profile
self.clippings = []
for rel in element.ConnectedTo:
connection = rel.RelatingConnectionType
other = tool.Ifc.get_object(rel.RelatedElement)
if connection not in ["ATPATH", "NOTDEFINED"]:
self.join(
obj, other, connection, rel.RelatedConnectionType, is_relating=True, description=rel.Description
)
for rel in element.ConnectedFrom:
connection = rel.RelatedConnectionType
other = tool.Ifc.get_object(rel.RelatingElement)
if connection not in ["ATPATH", "NOTDEFINED"]:
self.join(
obj, other, connection, rel.RelatingConnectionType, is_relating=False, description=rel.Description
)
new_matrix = copy.deepcopy(obj.matrix_world)
new_matrix.col[3] = self.body[0].to_4d().copy()
new_matrix.invert()
for clipping in self.clippings:
if clipping["operand_type"] == "IfcHalfSpaceSolid":
clipping["matrix"] = new_matrix @ clipping["matrix"]
self.clippings.extend(tool.Model.get_manual_booleans(element))
depth = (self.body[1] - self.body[0]).length
if self.axis_context:
axis = [(new_matrix @ a) for a in self.axis]
new_axis = ifcopenshell.api.run(
"geometry.add_axis_representation", tool.Ifc.get(), context=self.axis_context, axis=axis
)
old_axis = ifcopenshell.util.representation.get_representation(element, "Model", "Axis", "GRAPH_VIEW")
if old_axis:
for inverse in tool.Ifc.get().get_inverse(old_axis):
ifcopenshell.util.element.replace_attribute(inverse, old_axis, new_axis)
blenderbim.core.geometry.remove_representation(
tool.Ifc, tool.Geometry, obj=obj, representation=old_axis
)
else:
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_axis
)
new_body = ifcopenshell.api.run(
"geometry.add_profile_representation",
tool.Ifc.get(),
context=self.body_context,
profile=self.profile,
depth=depth,
cardinal_point=usage.CardinalPoint if usage else None,
clippings=self.clippings,
)
old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if old_body:
for inverse in tool.Ifc.get().get_inverse(old_body):
ifcopenshell.util.element.replace_attribute(inverse, old_body, new_body)
obj.data.BIMMeshProperties.ifc_definition_id = int(new_body.id())
obj.data.name = f"{self.body_context.id()}/{new_body.id()}"
blenderbim.core.geometry.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_body)
else:
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_body
)
previous_matrix = obj.matrix_world.copy()
previous_origin = obj.location.copy()
obj.location[0], obj.location[1], obj.location[2] = self.body[0]
bpy.context.view_layer.update()
if tool.Ifc.is_moved(obj):
# Openings should move with the host overall ...
# ... except their position should stay the same along the local Z axis of the wall
for opening in [r.RelatedOpeningElement for r in element.HasOpenings]:
percent = tool.Cad.edge_percent(
self.body[0], (previous_origin, (previous_matrix @ Vector((0, 0, 1))))
)
is_z_offset_increased = True if percent < 0 else False
change_in_z = (self.body[0] - previous_origin).length / self.unit_scale
coordinates = list(opening.ObjectPlacement.RelativePlacement.Location.Coordinates)
if is_z_offset_increased:
coordinates[2] += change_in_z
else:
coordinates[2] -= change_in_z
opening.ObjectPlacement.RelativePlacement.Location.Coordinates = coordinates
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
blenderbim.core.geometry.switch_representation(
tool.Geometry,
obj=obj,
representation=new_body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
tool.Geometry.record_object_materials(obj)
def join(self, profile1, profile2, connection1, connection2, is_relating=True, description="BUTT"):
element1 = tool.Ifc.get_entity(profile1)
element2 = tool.Ifc.get_entity(profile2)
axis1 = self.get_profile_axis(profile1)
axis2 = self.get_profile_axis(profile2)
angle = tool.Cad.angle_edges(axis1, axis2, signed=False, degrees=True)
if tool.Cad.is_x(angle, (0, 180), tolerance=0.001):
return False
intersect, _ = tool.Cad.intersect_edges(axis1, axis2)
proposed_axis = [self.axis[0], intersect] if connection1 == "ATEND" else [intersect, self.axis[1]]
if tool.Cad.is_x(tool.Cad.angle_edges(self.axis, proposed_axis, degrees=True), 180):
# The user has moved the element into an invalid position that cannot connect at the desired end
return False
self.axis[1 if connection1 == "ATEND" else 0] = intersect
# Work out body extents
if connection1 == "ATEND":
axisl = (profile2.matrix_world.inverted() @ axis1[1]) - (profile2.matrix_world.inverted() @ axis1[0])
elif connection1 == "ATSTART":
axisl = (profile2.matrix_world.inverted() @ axis1[0]) - (profile2.matrix_world.inverted() @ axis1[1])
xy_angle = degrees(Vector((1, 0)).angle_signed(axisl.normalized().to_2d()))
if xy_angle >= -135 and xy_angle <= -45:
closest_plane = "bottom"
furthest_plane = "top"
elif xy_angle >= 45 and xy_angle <= 135:
closest_plane = "top"
furthest_plane = "bottom"
elif xy_angle >= -45 and xy_angle <= 45:
closest_plane = "left"
furthest_plane = "right"
else:
closest_plane = "right"
furthest_plane = "left"
is_orthogonal = tool.Cad.is_x(tool.Cad.angle_edges(axis1, axis2, degrees=True), 90, tolerance=0.001)
if description == "MITRE":
# Mitre joints are an unofficial convention
# Check the closest plane from the perspective of the other element
if connection2 == "ATEND":
axisl = (profile1.matrix_world.inverted() @ axis2[1]) - (profile1.matrix_world.inverted() @ axis2[0])
elif connection2 == "ATSTART":
axisl = (profile1.matrix_world.inverted() @ axis2[0]) - (profile1.matrix_world.inverted() @ axis2[1])
xy_angle2 = degrees(Vector((1, 0)).angle_signed(axisl.normalized().to_2d()))
if xy_angle2 >= -135 and xy_angle2 <= -45:
closest_plane2 = "bottom"
furthest_plane2 = "top"
elif xy_angle2 >= 45 and xy_angle2 <= 135:
closest_plane2 = "top"
furthest_plane2 = "bottom"
elif xy_angle2 >= -45 and xy_angle2 <= 45:
closest_plane2 = "left"
furthest_plane2 = "right"
else:
closest_plane2 = "right"
furthest_plane2 = "left"
if connection1 == "ATEND":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
)
self.body[1] = intersect
else:
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
)
max_dim = self.get_max_bound_box_dimension(profile1)
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
# Mitre clip
if connection1 == connection2:
plane1 = self.get_profile_plane(profile1, furthest_plane2)
plane2 = self.get_profile_plane(profile2, furthest_plane)
clip1, direction1 = mathutils.geometry.intersect_plane_plane(
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
)
plane1 = self.get_profile_plane(profile1, closest_plane2)
plane2 = self.get_profile_plane(profile2, closest_plane)
clip2, direction2 = mathutils.geometry.intersect_plane_plane(
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
)
else:
plane1 = self.get_profile_plane(profile1, furthest_plane2)
plane2 = self.get_profile_plane(profile2, furthest_plane)
clip1, direction1 = mathutils.geometry.intersect_plane_plane(
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
)
plane1 = self.get_profile_plane(profile1, closest_plane2)
plane2 = self.get_profile_plane(profile2, closest_plane)
clip2, direction2 = mathutils.geometry.intersect_plane_plane(
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
)
y_axis = direction2
x_axis = (clip2 - clip1).normalized().to_3d()
z_axis = x_axis.cross(y_axis)
self.clippings.append(
{
"type": "IfcBooleanClippingResult",
"operand_type": "IfcHalfSpaceSolid",
"matrix": self.create_matrix(clip1, x_axis, y_axis, z_axis),
}
)
elif connection1 == "ATSTART":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
)
self.body[0] = intersect
else:
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
)
max_dim = self.get_max_bound_box_dimension(profile1)
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
if connection1 == connection2:
plane1 = self.get_profile_plane(profile1, furthest_plane2)
plane2 = self.get_profile_plane(profile2, furthest_plane)
clip1, direction1 = mathutils.geometry.intersect_plane_plane(
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
)
plane1 = self.get_profile_plane(profile1, closest_plane2)
plane2 = self.get_profile_plane(profile2, closest_plane)
clip2, direction2 = mathutils.geometry.intersect_plane_plane(
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
)
else:
plane1 = self.get_profile_plane(profile1, furthest_plane2)
plane2 = self.get_profile_plane(profile2, furthest_plane)
clip1, direction1 = mathutils.geometry.intersect_plane_plane(
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
)
plane1 = self.get_profile_plane(profile1, closest_plane2)
plane2 = self.get_profile_plane(profile2, closest_plane)
clip2, direction2 = mathutils.geometry.intersect_plane_plane(
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
)
y_axis = direction2
x_axis = (clip2 - clip1).normalized().to_3d()
z_axis = x_axis.cross(y_axis)
self.clippings.append(
{
"type": "IfcBooleanClippingResult",
"operand_type": "IfcHalfSpaceSolid",
"matrix": self.create_matrix(clip1, x_axis, y_axis, z_axis),
}
)
else:
# This is the standard L and T joints described by IFC
if connection1 == "ATEND":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
)
self.body[1] = intersect
else:
plane = self.get_profile_plane(
profile2,
furthest_plane if is_relating else closest_plane,
z_inwards=False if is_relating else True,
)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
)
max_dim = self.get_max_bound_box_dimension(profile1)
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
self.clippings.append(
{
"type": "IfcBooleanClippingResult",
"operand_type": "IfcHalfSpaceSolid",
"matrix": plane,
}
)
elif connection1 == "ATSTART":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
)
self.body[0] = intersect
else:
plane = self.get_profile_plane(
profile2,
furthest_plane if is_relating else closest_plane,
z_inwards=False if is_relating else True,
)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
)
max_dim = self.get_max_bound_box_dimension(profile1)
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
self.clippings.append(
{
"type": "IfcBooleanClippingResult",
"operand_type": "IfcHalfSpaceSolid",
"matrix": plane,
}
)
def get_max_bound_box_dimension(self, obj):
x = [v[0] for v in obj.bound_box]
y = [v[1] for v in obj.bound_box]
x_dim = max(x) - min(x)
y_dim = max(y) - min(y)
return x_dim if x_dim > y_dim else y_dim
def get_profile_plane(self, obj, plane, z_inwards=True):
# Get a matrix for one of the bounding planes of the profile to be used as cutting plane
# Here's an example of looking at the end of an I-Beam profile with a +Z extrusion out of the screen
# top
# +-----+ Y
# |=====| ^
# | | | |
# left | | | right Z->X
# | | |
# |=====|
# +-----+
# bottom
if plane == "top":
max_y = max([v[1] for v in obj.bound_box])
p = obj.matrix_world @ Vector((0, max_y, 0))
x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))
if z_inwards:
y_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((0, -1, 0))
else:
y_axis = obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0))
elif plane == "bottom":
min_y = min([v[1] for v in obj.bound_box])
p = obj.matrix_world @ Vector((0, min_y, 0))
x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))
if z_inwards:
y_axis = obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0))
else:
y_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((0, -1, 0))
elif plane == "right":
max_x = max([v[0] for v in obj.bound_box])
p = obj.matrix_world @ Vector((max_x, 0, 0))
x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))
if z_inwards:
y_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
else:
y_axis = obj.matrix_world.to_quaternion() @ Vector((0, -1, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
elif plane == "left":
min_x = min([v[0] for v in obj.bound_box])
p = obj.matrix_world @ Vector((min_x, 0, 0))
x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))
if z_inwards:
y_axis = obj.matrix_world.to_quaternion() @ Vector((0, -1, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
else:
y_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
return self.create_matrix(p, x_axis, y_axis, z_axis)
def create_matrix(self, p, x, y, z):
return Matrix(
(
(x[0], y[0], z[0], p[0]),
(x[1], y[1], z[1], p[1]),
(x[2], y[2], z[2], p[2]),
(0.0, 0.0, 0.0, 1.0),
)
)
def get_profile_axis(self, obj):
z_values = [v[2] for v in obj.bound_box]
return [
(obj.matrix_world @ Vector((0.0, 0.0, min(z_values)))),
(obj.matrix_world @ Vector((0.0, 0.0, max(z_values)))),
]
class RecalculateProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.recalculate_profile"
bl_label = "Recalculate Profile"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
DumbProfileRecalculator().recalculate(context.selected_objects)
return {"FINISHED"}
class DumbProfileRecalculator:
def recalculate(self, profiles):
queue = set()
for profile in profiles:
element = tool.Ifc.get_entity(profile)
queue.add((element, profile))
for rel in getattr(element, "ConnectedTo", []):
queue.add((rel.RelatedElement, tool.Ifc.get_object(rel.RelatedElement)))
for rel in getattr(element, "ConnectedFrom", []):
queue.add((rel.RelatingElement, tool.Ifc.get_object(rel.RelatingElement)))
joiner = DumbProfileJoiner()
for element, profile in queue:
if profile:
joiner.recreate_profile(element, profile)
class ChangeProfileDepth(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.change_profile_depth"
bl_label = "Change Profile Length"
bl_options = {"REGISTER", "UNDO"}
depth: bpy.props.FloatProperty()
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
joiner = DumbProfileJoiner()
for obj in context.selected_objects:
joiner.set_depth(obj, self.depth)
return {"FINISHED"}
class ChangeCardinalPoint(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.change_cardinal_point"
bl_label = "Change Cardinal Point"
bl_options = {"REGISTER", "UNDO"}
cardinal_point: bpy.props.IntProperty()
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
objs = []
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
material = ifcopenshell.util.element.get_material(element, should_skip_usage=False)
if material.is_a("IfcMaterialProfileSetUsage"):
material.CardinalPoint = self.cardinal_point
objs.append(obj)
DumbProfileRecalculator().recalculate(objs)
return {"FINISHED"}
class Rotate90(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.rotate_90"
bl_label = "Rotate 90"
bl_options = {"REGISTER", "UNDO"}
axis: bpy.props.StringProperty()
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
objs = []
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if element.ConnectedTo or element.ConnectedFrom:
objs.append(obj)
rotate_matrix = Matrix.Rotation(pi / 2, 4, self.axis)
obj.matrix_world @= rotate_matrix
bpy.context.view_layer.update()
DumbProfileRecalculator().recalculate(objs)
return {"FINISHED"}