Files
IfcOpenShell/src/blenderbim/blenderbim/bim/module/model/mep.py
T
Andrej730 dd02925cc4 Fixed bug adding ports to the transitions between profiles with an offset
end port ended up located without the offset
2024-04-16 10:25:01 +02:00

811 lines
36 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 math
import collections
import bmesh
import re
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.unit
import ifcopenshell.util.system
import ifcopenshell.util.element
import ifcopenshell.util.representation
import mathutils.geometry
import numpy as np
import blenderbim.bim.handler
import blenderbim.core.type
import blenderbim.core.root
import blenderbim.core.geometry
import blenderbim.tool as tool
from math import pi, degrees, radians
from copy import copy
from mathutils import Vector, Matrix
from ifcopenshell.util.shape_builder import ShapeBuilder
from blenderbim.bim.module.model.profile import DumbProfileJoiner
V = lambda *x: Vector([float(i) for i in x])
class RegenerateDistributionElement(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.regenerate_distribution_element"
bl_description = (
"Regenerates the positions and segment lengths of a distribution element and all connected elements."
)
bl_label = "Regenerate Distribution Element"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
current_element = tool.Ifc.get_entity(bpy.context.active_object)
processed_elements = set()
# The goal is to regenerate all recursively connected elements that
# minimise movement as much as possible.
# A queue is a list of branches. A branch is a list of elements in
# sequence, each one connecting to another element. An element in a
# branch may have a child queue. The queue and child queues are
# acyclic.
def extend_branch(element, branch, predecessor=None):
processed_elements.add(element)
branch_element = {"element": element, "children": [], "predecessor": predecessor}
branch.append(branch_element)
connected = {e for e in ifcopenshell.util.system.get_connected_to(element) if e not in processed_elements}
connected.update(
[e for e in ifcopenshell.util.system.get_connected_from(element) if e not in processed_elements]
)
for connected_element in connected:
branch_element["children"].append(extend_branch(connected_element, [], element))
return branch
extended_branch = extend_branch(current_element, [])
queue = extended_branch[0]["children"]
# import pprint
# pprint.pprint(queue)
def get_connected_ports_between(element1, element2):
ports1 = tool.System.get_ports(element1)
ports2 = tool.System.get_ports(element2)
for p in ports1:
connected_port = tool.System.get_connected_port(p)
# in IFC2X3 there is no PredefinedType
if getattr(p, "PredefinedType", None) == "WIRELESS":
continue
if connected_port in ports2:
return p, connected_port
return None, None
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
def process_branch(branch):
for branch_element in branch:
element = branch_element["element"]
print("processing", element)
predecessor = branch_element["predecessor"]
# Perform the extend, translate, rotate, etc the element as necessary based on the predecessor.
# For everything besides segments, only translate. No rotation.
obj = tool.Ifc.get_object(element)
obj_pred = tool.Ifc.get_object(predecessor)
if tool.Ifc.is_moved(obj):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
if tool.Ifc.is_moved(obj_pred):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj_pred)
port, port_pred = get_connected_ports_between(element, predecessor)
port_matrix_pred = tool.Model.get_element_matrix(port_pred)
# Only segments can be extended
# extension for them takes priority over translation
if element.is_a("IfcFlowSegment"):
DumbProfileJoiner().join_E(obj, port_matrix_pred.translation * si_conversion)
context.view_layer.update() # update since extrusion might involve changing object's location
port_martix = tool.Model.get_element_matrix(port)
port_location = port_martix.translation
port_location_pred = port_matrix_pred.translation
if not tool.Cad.are_vectors_equal(port_location, port_location_pred):
obj.location += (port_location_pred - port_location) * si_conversion
context.view_layer.update() # otherwise tool.Ifc.is_moved won't get triggered
else:
# If the element does not need to be transformed, return early.
return
for child_branch in branch_element["children"]:
process_branch(child_branch)
for branch in queue:
process_branch(branch)
class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.fit_flow_segments"
bl_description = "Add a fitting based on currently selected elements and cursor"
bl_label = "Fit Flow Segments"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
# TODO: need to add ui for parameters:
# - obstruction cap thickness
# - start/end thickness and angle for transition
selected_objs = []
selected_profiles = []
selected_class = None
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if element and element.is_a("IfcFlowSegment"):
if selected_class and not element.is_a(selected_class):
return # The user is mixing up ducts and pipes.
profile = tool.Model.get_flow_segment_profile(element)
if profile:
selected_profiles.append(profile)
selected_objs.append(obj)
selected_class = element.is_a()
total_selected_objs = len(selected_objs)
total_profiles = len(set(selected_profiles))
fitting_type = None
if total_selected_objs == 1:
fitting_type = "OBSTRUCTION"
bpy.ops.bim.mep_add_obstruction()
elif total_selected_objs == 2:
# Shorten the axis by the profile size to allow for fuzzy intersections
# e.g. if two ducts touch, we want a bend, not a cross.
axis1 = tool.Model.get_flow_segment_axis(selected_objs[0])
profile_size = max(selected_objs[0].dimensions.x, selected_objs[0].dimensions.y)
offset = (axis1[1] - axis1[0]).normalized() * profile_size
axis1 = (axis1[0] + offset, axis1[1] - offset)
axis2 = tool.Model.get_flow_segment_axis(selected_objs[1])
profile_size = max(selected_objs[1].dimensions.x, selected_objs[1].dimensions.y)
offset = (axis2[1] - axis2[0]).normalized() * profile_size
axis2 = (axis2[0] + offset, axis2[1] - offset)
angle = tool.Cad.angle_edges(axis1, axis2, signed=False, degrees=True)
is_parallel = tool.Cad.is_x(angle, (0, 180), tolerance=0.001)
if total_profiles == 1:
if is_parallel:
return
intersect1, intersect2 = tool.Cad.intersect_edges(axis1, axis2)
is_on_axis1 = tool.Cad.is_point_on_edge(intersect1, axis1)
is_on_axis2 = tool.Cad.is_point_on_edge(intersect2, axis2)
if not is_on_axis1 and not is_on_axis2:
fitting_type = "BEND"
elif is_on_axis1 and is_on_axis2:
fitting_type = "CROSS"
else:
fitting_type = "TEE"
elif total_profiles == 2:
if is_parallel:
fitting_type = "TRANSITION"
bpy.ops.bim.mep_add_transition()
elif total_selected_objs == 3:
if total_profiles > 1:
return
axis1 = tool.Model.get_flow_segment_axis(selected_objs[0])
axis2 = tool.Model.get_flow_segment_axis(selected_objs[1])
axis3 = tool.Model.get_flow_segment_axis(selected_objs[2])
angle12 = tool.Cad.angle_edges(axis1, axis2, signed=False, degrees=True)
angle13 = tool.Cad.angle_edges(axis1, axis3, signed=False, degrees=True)
angle21 = tool.Cad.angle_edges(axis2, axis1, signed=False, degrees=True)
angle23 = tool.Cad.angle_edges(axis2, axis3, signed=False, degrees=True)
is_parallel12 = tool.Cad.is_x(angle12, (0, 180), tolerance=0.001)
is_parallel13 = tool.Cad.is_x(angle13, (0, 180), tolerance=0.001)
is_parallel21 = tool.Cad.is_x(angle21, (0, 180), tolerance=0.001)
is_parallel23 = tool.Cad.is_x(angle23, (0, 180), tolerance=0.001)
if not all(is_parallel12, is_parallel13, is_parallel21, is_parallel23):
fitting_type = "WYE"
if not fitting_type:
return
print(fitting_type)
class MEPGenerator:
def __init__(self, relating_type=None):
self.relating_type = relating_type
def setup_ports(self, obj):
self.file = tool.Ifc.get()
self.collection = bpy.context.view_layer.active_layer_collection.collection
segment = tool.Ifc.get_entity(obj)
representation = ifcopenshell.util.representation.get_representation(segment, "Model", "Body", "MODEL_VIEW")
extrusion = tool.Model.get_extrusion(representation)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
length = extrusion.Depth * si_conversion
start_port_matrix = obj.matrix_world @ Matrix()
end_port_matrix = obj.matrix_world @ Matrix.Translation((0, 0, length))
ports = tool.System.get_ports(segment)
if segment.is_a("IfcFlowSegment") and not ports:
tool.System.add_ports(obj)
return
# adjust current segment ports and related flow segments
segment_data = self.get_segment_data(segment)
for port_position in ("start_port", "end_port"):
port = segment_data.get(port_position, None)
if not port:
continue
# no need to correct start port position - it's corrected automatically
# as DumbProfileJoiner already moved the general segment position in that case
if port_position == "end_port":
tool.Model.edit_element_placement(port, end_port_matrix)
continue
# NOTE: currently this functionality is moved to bim.regenerate_distribution_element
connected_port = tool.System.get_connected_port(port)
if not connected_port:
continue
# handle only obstructions for now
connected_element = tool.System.get_port_relating_element(connected_port)
def get_predefined_type(element):
element_type = ifcopenshell.util.element.get_type(element)
if element_type:
return element_type.PredefinedType
return element.PredefinedType
connected_obj = tool.Ifc.get_object(connected_element)
connected_element_length = connected_obj.dimensions.z
if (segment.is_a("IfcFlowSegment") and get_predefined_type(connected_element) == "OBSTRUCTION") or (
segment.is_a("IfcFlowFitting") and connected_element.is_a("IfcFlowSegment")
):
if port_position == "start_port":
if segment.is_a("IfcFlowFitting"):
connected_element_length = (
tool.Model.get_flow_segment_axis(connected_obj)[0]
- tool.Model.get_flow_segment_axis(obj)[0]
).length
connected_port_matrix = start_port_matrix @ Matrix.Translation((0, 0, -connected_element_length))
else:
connected_port_matrix = end_port_matrix
connected_obj.matrix_world = connected_port_matrix
if port_position == "start_port" and segment.is_a("IfcFlowFitting"):
profile_joiner = DumbProfileJoiner()
profile_joiner.set_depth(connected_obj, connected_element_length)
def get_segment_data(self, segment):
ports = tool.System.get_ports(segment)
segment_object = tool.Ifc.get_object(segment)
start_point = segment_object.location
extrusion_depth = segment_object.dimensions.z
end_point = segment_object.matrix_world @ V(0, 0, extrusion_depth)
segment_data = {
"start_point": start_point.copy().freeze(),
"end_point": end_point.freeze(),
"ports": ports,
"extrusion_depth": extrusion_depth,
}
for port in ports:
port_local_position = V(*port.ObjectPlacement.RelativePlacement.Location.Coordinates)
if tool.Cad.is_x(port_local_position.length, 0.0):
segment_data["start_port"] = port
else:
segment_data["end_port"] = port
return segment_data
def get_mep_element_class_name(self, element, mep_class_type):
split_camel_case = lambda x: re.findall("[A-Z][^A-Z]*", x)
class_name = "".join(split_camel_case(element.is_a())[:-1] + [mep_class_type])
return class_name
def get_compatible_fitting_type(self, segment_or_segments, port_or_ports, predefined_type):
"""
returns a dict of compatible fitting_type and start_port_match flag to correctly place the fitting.
We find compatible fitting only by checking
if they were already used with that segment type before
and fitting's ports should match `port_or_ports` by PredefinedType and SystemType.
If port from `port_or_ports` has PredefinedType/SystemType == None/NOTDEFINED then
those parameters won't be taken into account checking compatibility.
There lies the problem that it won't be
able to identify the fittings that were not yet connected to any segments yet.
"""
# TODO: check angle, start, end and offset for transitions
if not isinstance(segment_or_segments, collections.abc.Iterable):
segments = [segment_or_segments]
ports = [port_or_ports]
else:
segments = segment_or_segments
ports = port_or_ports
segments_data = []
for segment, port in zip(segments, ports, strict=True):
segment_type = ifcopenshell.util.element.get_type(segment)
# if segment doesn't have type we cannot check compatibility by available occurrences
if segment_type is None:
return
segments_data.append((segment_type, port.PredefinedType, port.SystemType))
def are_connected_elements_compatible(segments_data, fitting_data):
# prevent arguments mutation, not using deepcopy because of the errors with ifc elements
segments_data = [copy(i) for i in segments_data]
fitting_data = [copy(i) for i in fitting_data]
not_defined_values = {"NOTDEFINED", None}
if len(segments_data) != len(fitting_data):
return False
def are_segments_compatible(test_segment_data, base_segment_data):
segment_type, predefined_type, system_type = test_segment_data
base_segment_type, base_predefined_type, base_system_type = base_segment_data
if segment_type != base_segment_type:
return False
if predefined_type not in not_defined_values and predefined_type != base_predefined_type:
return False
if system_type not in not_defined_values and system_type != base_system_type:
return False
return True
# NOTE: I have a feeling that there are cases where order
# in which we're checking the segments is important
# but I couldn't pin it down exact cases
for test_segment_data in fitting_data[:]:
for base_segment_data in segments_data:
if not are_segments_compatible(test_segment_data, base_segment_data):
continue
segments_data.remove(test_segment_data)
# all segments were sorted
return len(segments_data) == 0
def pack_return_data(fitting_type, ports, segments_data):
for port in ports:
port_local_position = V(*port.ObjectPlacement.RelativePlacement.Location.Coordinates)
if tool.Cad.is_x(port_local_position.length, 0.0):
start_port = port
break
connected_port = tool.System.get_connected_port(start_port)
connected_element = tool.System.get_port_relating_element(connected_port)
element_type = ifcopenshell.util.element.get_type(connected_element)
return {"fitting_type": fitting_type, "start_port_match": element_type == segments_data[0][0]}
fitting_types = tool.Ifc.get().by_type(self.get_mep_element_class_name(segments[0], "FittingType"))
for fitting_type in fitting_types:
if fitting_type.PredefinedType != predefined_type:
continue
fittings = tool.Ifc.get_all_element_occurrences(fitting_type)
if not fittings:
continue
for fitting in fittings:
ports = ifcopenshell.util.system.get_ports(fitting)
fitting_data = []
skipped_the_occurrence = False
for port in ports:
connected_port = tool.System.get_connected_port(port)
# fitting port is not connected to anything
if not connected_port:
skipped_the_occurrence = True
break
connected_element = tool.System.get_port_relating_element(connected_port)
element_type = ifcopenshell.util.element.get_type(connected_element)
# fitting is connected to none type
if element_type is None:
skipped_the_occurrence = True
break
fitting_data.append((element_type, port.PredefinedType, port.SystemType))
# if we skipped the occurrence we still can other occurrences
# otherwise checking 1 occurrence is enough
if not skipped_the_occurrence:
if are_connected_elements_compatible(segments_data, fitting_data):
return pack_return_data(fitting_type, ports, segments_data)
return
def create_obstruction_type(self, segment):
# code is very similar to "bim.add_type"
profile_set = ifcopenshell.util.element.get_material(segment, should_skip_usage=True)
material_profile = profile_set.MaterialProfiles[0]
profile = material_profile.Profile
material = material_profile.Material
ifc_class = self.get_mep_element_class_name(segment, "FittingType")
ifc_file = tool.Ifc.get()
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
obj = bpy.data.objects.new("Obstruction", None)
# TODO: OBSTRUCTION predefined type is available only for IfcDuctFitting and IfcPipeFitting
element = blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class=ifc_class,
predefined_type="OBSTRUCTION",
should_add_representation=True,
context=body,
ifc_representation_class=None,
)
rel = ifcopenshell.api.run("material.assign_material", ifc_file, product=element, type="IfcMaterialProfileSet")
profile_set = rel.RelatingMaterial
material_profile = ifcopenshell.api.run(
"material.add_profile", ifc_file, profile_set=profile_set, material=material
)
ifcopenshell.api.run("material.assign_profile", ifc_file, material_profile=material_profile, profile=profile)
return element
def add_obstruction(self, segment, length, at_segment_start=False):
"""
`segment` is a segment ifc element
`length` is obstruction length provided in si units
returns `(None, error_message)` if there was some error in the process
or returns `(obstruction_element, None)` if everything went fine.
"""
related_port_name = "start" if at_segment_start else "end"
segment_data = self.get_segment_data(segment)
related_port = segment_data[f"{related_port_name}_port"]
# communicate error cases
if related_port.ConnectedTo or related_port.ConnectedFrom:
return None, f"Failed to add obstruction - {related_port_name} port is already connected."
if length >= segment_data["extrusion_depth"]:
return None, "Failed to add obstruction - obstruction length is larger than the segment."
segment_obj = tool.Ifc.get_object(segment)
segment_matrix = segment_obj.matrix_world
segment_rotation = segment_matrix.to_quaternion()
fitting_data = self.get_compatible_fitting_type(segment, related_port, "OBSTRUCTION")
obstruction_type = fitting_data["fitting_type"] if fitting_data else None
if not obstruction_type:
obstruction_type = self.create_obstruction_type(segment)
profile_joiner = DumbProfileJoiner()
# create obstruction occurrence and setup it's length and port
# NOTE: at this point we loose current blender objects selection
bpy.ops.bim.add_constr_type_instance(relating_type_id=obstruction_type.id())
obstruction_obj = bpy.context.active_object
obstruction_obj.matrix_world = segment_matrix
profile_joiner.set_depth(obstruction_obj, length)
obstruction_port = tool.System.add_ports(
obstruction_obj,
add_start_port=not at_segment_start,
add_end_port=at_segment_start,
)[0]
# change segment length
new_segment_length = segment_data["extrusion_depth"] - length
profile_joiner.set_depth(segment_obj, new_segment_length)
if at_segment_start:
segment_obj.location += segment_rotation @ V(0, 0, length)
else:
obstruction_obj.location += segment_rotation @ V(0, 0, new_segment_length)
tool.Ifc.run("system.connect_port", port1=related_port, port2=obstruction_port, direction="NOTDEFINED")
obstruction = tool.Ifc.get_entity(obstruction_obj)
return obstruction, None
class MEPAddObstruction(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.mep_add_obstruction"
bl_label = "Add Obstruction"
bl_description = "Adds obstruction to the MEP segment"
bl_options = {"REGISTER", "UNDO"}
length: bpy.props.FloatProperty(
name="Obstruction Length", description="Obstruction length in SI units", default=0.1, subtype="DISTANCE"
)
segment_id: bpy.props.IntProperty(name="Segment Element ID", default=0)
def _execute(self, context):
if self.segment_id:
element = tool.Ifc.get().by_id(self.segment_id)
else:
element = tool.Ifc.get_entity(context.active_object)
if not element:
return {"CANCELLED"}
if not element.is_a("IfcFlowSegment"):
self.report({"ERROR"}, f"Failed to add obstruction - object is not a MEP segment: {element.is_a()}.")
return {"CANCELLED"}
# derive obstruction position from the cursor
cursor_location = bpy.context.scene.cursor.location
obj = tool.Ifc.get_object(element)
axis = tool.Model.get_flow_segment_axis(obj)
# check if cursor is closer to the segment start
at_segment_start = tool.Cad.edge_percent(cursor_location, axis) < 0.5
obstruction, error_msg = MEPGenerator().add_obstruction(element, self.length, at_segment_start)
if error_msg:
self.report({"ERROR"}, error_msg)
return {"CANCELLED"}
return {"FINISHED"}
class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.mep_add_transition"
bl_label = "Add Transition"
bl_description = (
"Adds transition between two MEP elements. Elements are either provided by ID or selected in Blender"
)
bl_options = {"REGISTER", "UNDO"}
start_length: bpy.props.FloatProperty(
name="Start Length", description="Transition start length in SI units", default=0.1, subtype="DISTANCE"
)
end_length: bpy.props.FloatProperty(
name="End Length", description="Transition end length in SI units", default=0.1, subtype="DISTANCE"
)
start_segment_id: bpy.props.IntProperty(name="Start Segment Element ID", default=0)
end_segment_id: bpy.props.IntProperty(name="End Segment Element ID", default=0)
def _execute(self, context):
start_element, end_element = None, None
ifc_file = tool.Ifc.get()
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
if self.start_segment_id and self.end_segment_id:
start_element = ifc_file.by_id(self.start_segment_id)
end_element = ifc_file.by_id(self.end_segment_id)
start_object = tool.Ifc.get_object(start_element)
end_object = tool.Ifc.get_object(end_element)
elif len(context.selected_objects) == 2:
start_object = context.active_object
end_object = next(o for o in context.selected_objects if o != context.active_object)
start_element = tool.Ifc.get_entity(start_object)
end_element = tool.Ifc.get_entity(end_object)
if not start_element or not end_element:
self.report({"ERROR"}, f"Two IFC elements should be selected for the transition")
return {"CANCELLED"}
else:
self.report({"ERROR"}, f"Two IFC elements should be provided for the transition")
return {"CANCELLED"}
# TODO: support IfcFlowTerminal
def is_mep(element):
return element.is_a("IfcFlowSegment") or element.is_a("IfcFlowFitting")
if not is_mep(start_element) or not is_mep(end_element):
self.report(
{"ERROR"},
f"Failed to add transition - some object is not a MEP element: {start_element.is_a()}, {end_element.is_a()}.",
)
return {"CANCELLED"}
start_axis = tool.Model.get_flow_segment_axis(start_object)
end_axis = tool.Model.get_flow_segment_axis(end_object)
start_object_rotation = start_object.matrix_world.to_quaternion()
start_object_z_basis = start_object_rotation.to_matrix().col[2] # z basis vector
keep_only_z_axis = lambda p_ws: p_ws.dot(start_object_z_basis) * start_object_z_basis
# TODO: support cases when segments are partially or completely overlapping each other
if not tool.Cad.are_edges_parallel(start_axis, end_axis):
self.report({"ERROR"}, f"Failed to add transition - segments are not parallel.")
return {"CANCELLED"}
start_segment_data = MEPGenerator().get_segment_data(start_element)
end_segment_data = MEPGenerator().get_segment_data(end_element)
end_port = end_segment_data["start_port"]
start_port = start_segment_data["end_port"]
points_ports_map = {
start_segment_data["start_point"]: start_segment_data["start_port"],
start_segment_data["end_point"]: start_segment_data["end_port"],
end_segment_data["start_point"]: end_segment_data["start_port"],
end_segment_data["end_point"]: end_segment_data["end_port"],
}
# transition points
start_point, end_point = tool.Cad.closest_points(
(start_segment_data["start_point"], start_segment_data["end_point"]),
(end_segment_data["start_point"], end_segment_data["end_point"]),
)
# figure profile offset
base_transition_dir = keep_only_z_axis(end_point - start_point).normalized()
flip_profile_offset = base_transition_dir.dot(start_object_z_basis) < 0
if tool.Cad.are_edges_collinear(start_axis, end_axis):
profile_offset = None
else:
to_start_object_space = start_object_rotation.inverted()
profile_offset = (
(to_start_object_space @ end_object.location) - (to_start_object_space @ start_object.location)
).xy
if tool.Cad.is_x(profile_offset.length_squared, 0):
profile_offset = None
else:
profile_offset = profile_offset / si_conversion
if flip_profile_offset:
profile_offset *= V(1, -1)
# world space profile offset
profile_offset_ws = (
start_object_rotation @ (profile_offset * si_conversion).to_3d() if profile_offset else V(0, 0, 0)
)
# will need entire_length to check that transition length fill fit
first_segment_start, second_segment_end = [
p
for p in (
start_segment_data["start_point"],
start_segment_data["end_point"],
end_segment_data["start_point"],
end_segment_data["end_point"],
)
if p not in (start_point, end_point)
]
def get_segments_length():
start_dir = (start_point - first_segment_start).normalized()
segments_vector = second_segment_end - first_segment_start
return segments_vector.dot(start_dir)
entire_length = get_segments_length()
# can't rely on (end_point-start_point) here because
# transition might change the segments length and therefore direction will be changed
segments_dir = (start_point - first_segment_start).normalized()
start_port = points_ports_map[start_point]
end_port = points_ports_map[end_point]
# add transition representation
builder = ShapeBuilder(ifc_file)
rep, transition_data = builder.mep_transition_shape(
start_element,
end_element,
self.start_length / si_conversion,
self.end_length / si_conversion,
profile_offset=profile_offset,
)
if not rep:
self.report({"ERROR"}, f"Failed to add transition - this kind of profiles is not yet supported.")
return {"CANCELLED"}
full_transition_length = transition_data["full_transition_length"] * si_conversion
if full_transition_length >= entire_length:
self.report(
{"ERROR"},
f"Failed to add transition - transition length is larger the segments and the distance between them.\n"
+ f"Transition length: {full_transition_length:.2f}m, segments length: {entire_length:.2f}m",
)
ifcopenshell.api.run("geometry.remove_representation", ifc_file, representation=rep)
return {"CANCELLED"}
# calculate bunch of points to for adjustments
middle_point = keep_only_z_axis((start_point + end_point) / 2 - start_point) + start_point
start_segment_extend_point = middle_point - segments_dir * full_transition_length / 2
end_segment_extend_point = middle_point + segments_dir * full_transition_length / 2 + profile_offset_ws
transition_dir = keep_only_z_axis(end_segment_extend_point - start_segment_extend_point).normalized()
# adjust the segments
end_object_rotation = end_object.matrix_world.to_quaternion()
end_object_z_basis = end_object_rotation.to_matrix().col[2] # z basis vector
if tool.Cad.is_x(start_object_z_basis.dot(transition_dir), 1):
start_connection = "ATEND"
else:
start_connection = "ATSTART"
if tool.Cad.is_x(end_object_z_basis.dot(transition_dir), 1):
end_connection = "ATSTART"
else:
end_connection = "ATEND"
DumbProfileJoiner().join_E(start_object, start_segment_extend_point, start_connection)
DumbProfileJoiner().join_E(end_object, end_segment_extend_point, end_connection)
# find the compatible fitting type
fitting_data = MEPGenerator().get_compatible_fitting_type(
[start_element, end_element], [start_port, end_port], "TRANSITION"
)
transition_type = fitting_data["fitting_type"] if fitting_data else None
if transition_type:
# TODO: handle the case without creating a representation in the first place?
ifcopenshell.api.run("geometry.remove_representation", ifc_file, representation=rep)
start_port_match = fitting_data["start_port_match"] if fitting_data else True
# create new fitting type if nothing is compatible
if not transition_type:
mesh = bpy.data.meshes.new("Transition")
obj = bpy.data.objects.new("Transition", mesh)
transition_type = blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class=MEPGenerator().get_mep_element_class_name(start_element, "FittingType"),
predefined_type="TRANSITION",
should_add_representation=False,
)
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
tool.Model.replace_object_ifc_representation(body, obj, rep)
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=transition_type, name="BBIM_Fitting")
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Data": json.dumps(transition_data, default=list)},
)
# NOTE: at this point we loose current blender objects selection
# create transition element
bpy.ops.bim.add_constr_type_instance(relating_type_id=transition_type.id())
transition_obj = bpy.context.active_object
# adjust transition segment rotation and location
# required since we'll base our `transition_obj_dir` on this
transition_obj.matrix_world = start_object.matrix_world
context.view_layer.update()
# depending on transition direction we may need to flip it or attach it's origin to end segment
# direction can be different depending on:
# - order of the current segments
# - order of the segments that were used with the same transition type before
transition_obj_dir = tool.Cad.get_edge_direction(tool.Model.get_flow_segment_axis(transition_obj))
direction_match = tool.Cad.are_vectors_equal(transition_dir, transition_obj_dir)
# if there are no mismatches or everything matches up we don't need to flip the transition
if start_port_match != direction_match:
transition_obj.matrix_world = start_object.matrix_world @ Matrix.Rotation(radians(180), 4, "X")
transition_obj.location = start_segment_extend_point if start_port_match else end_segment_extend_point
# add ports and connect them
ports = tool.System.add_ports(transition_obj, offset_end_port=profile_offset_ws)
if not start_port_match:
start_port, end_port = end_port, start_port
tool.Ifc.run("system.connect_port", port1=ports[0], port2=start_port, direction="NOTDEFINED")
tool.Ifc.run("system.connect_port", port1=ports[1], port2=end_port, direction="NOTDEFINED")
return {"FINISHED"}