operator to add mep obstruction

Currently it's not yet available from UI, only from F3 menu.
On changing segment's length obstruction is not yet adjusted.
Short demonstration - https://imgur.com/a/I5fybpO
This commit is contained in:
Andrej730
2023-08-01 17:42:58 +05:00
parent cbf8b7f3a2
commit 490937ecf9
5 changed files with 192 additions and 10 deletions
@@ -38,6 +38,7 @@ from . import (
door,
railing,
roof,
mep,
)
classes = (
@@ -172,6 +173,7 @@ classes = (
roof.EnableEditingRoofPath,
roof.RemoveRoof,
roof.SetGableRoofEdgeAngle,
mep.MEPAddObstruction,
)
addon_keymaps = []
@@ -32,9 +32,14 @@ import blenderbim.core.geometry
import blenderbim.tool as tool
from math import pi, degrees
from mathutils import Vector, Matrix
import re
from blenderbim.bim.module.model.profile import DumbProfileJoiner
V = lambda *x: Vector([float(i) for i in x])
float_is_zero = lambda f: 0.0001 >= f >= -0.0001
class MepGenerator:
class MEPGenerator:
def __init__(self, relating_type=None):
self.relating_type = relating_type
@@ -52,14 +57,186 @@ class MepGenerator:
ports = tool.System.get_ports(element)
if not ports:
start_port_matrix = Matrix()
# TODO: what are the best default directions here?
for mat, flow_direction in zip([start_port_matrix, end_port_matrix], ("SINK", "SOURCE")):
port = tool.Ifc.run("system.add_port", element=element)
port.FlowDirection = flow_direction
tool.Ifc.run("geometry.edit_object_placement", product=port, matrix=obj.matrix_world @ mat, is_si=True)
return
# TODO: better way to find the port to be moved
end_port = next((p for p in ports if p.FlowDirection == "SOURCE"), None)
if not end_port:
return
end_port = self.get_segment_data(element)["end_port"]
tool.Model.edit_element_placement(end_port, obj.matrix_world @ end_port_matrix)
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,
"end_point": end_point,
"ports": ports,
"extrusion_depth": extrusion_depth,
}
for port in ports:
port_local_position = V(*port.ObjectPlacement.RelativePlacement.Location.Coordinates)
if float_is_zero(port_local_position.length):
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())[:2] + [mep_class_type])
return class_name
def get_compatible_fitting_type(self, segment, predefined_type):
"""We find compatible fitting only by checking if they were
already used with that segment type before.
There lies the problem that it won't be
able to identify the fittings that were not connected to any segments yet.
"""
segment_type = ifcopenshell.util.element.get_type(segment)
if not segment_type:
return None
fitting_types = tool.Ifc.get().by_type(self.get_mep_element_class_name(segment, "Fitting"))
for fitting_type in fitting_types:
if fitting_type.PredefinedType != predefined_type:
continue
fittings = tool.Ifc.get_all_element_occurences(fitting_type)
if not fittings:
continue
fitting = fittings[0]
elements = set(
ifcopenshell.util.system.get_connected_to(fitting)
+ ifcopenshell.util.system.get_connected_from(fitting)
)
for element in elements:
if element.IsTypedBy and element.IsTypedBy[0].RelatingType == segment_type:
return fitting_type
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("Fitting", None)
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()
obstruction_type = self.get_compatible_fitting_type(segment, "OBSTRUCTION")
if not obstruction_type:
obstruction_type = self.create_obstruction_type(segment)
profile_joiner = DumbProfileJoiner()
# create obstruction occurence 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 = tool.Ifc.get_entity(obstruction_obj)
obstruction_port = tool.Ifc.run("system.add_port", element=obstruction)
port_local_position = Matrix.Translation((0, 0, length)) if at_segment_start else Matrix()
tool.Ifc.run(
"geometry.edit_object_placement",
product=obstruction_port,
matrix=segment_matrix @ port_local_position,
is_si=True,
)
# 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)
# TODO: what's best default direction here?
tool.Ifc.run("system.connect_port", port1=related_port, port2=obstruction_port, direction="SOURCE")
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)
at_segment_start: bpy.props.BoolProperty(
name="At Segment Start",
description="Whether to add obstruction at the segment start or at the end",
default=False,
)
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"}
obstruction, error_msg = MEPGenerator().add_obstruction(element, self.length, self.at_segment_start)
if error_msg:
self.report({"ERROR"}, error_msg)
return {"CANCELLED"}
return {"FINISHED"}
@@ -139,9 +139,10 @@ class AddConstrTypeInstance(bpy.types.Operator):
if material and material.is_a("IfcMaterialProfileSet"):
if obj := profile.DumbProfileGenerator(relating_type).generate():
tool.Blender.select_and_activate_single_object(context, obj)
if relating_type.is_a("IfcFlowSegmentType"):
self.set_flow_segment_rl(obj)
mep.MepGenerator(relating_type).setup_ports(obj)
mep.MEPGenerator(relating_type).setup_ports(obj)
return {"FINISHED"}
elif material and material.is_a("IfcMaterialLayerSet"):
if self.generate_layered_element(ifc_class, relating_type):
@@ -34,7 +34,6 @@ from mathutils import Vector, Matrix, Quaternion
from blenderbim.bim.module.geometry.helper import Helper
from blenderbim.bim.module.model.wall import DumbWallRecalculator
from blenderbim.bim.module.model.decorator import ProfileDecorator
from blenderbim.bim.module.model.mep import MepGenerator
class DumbProfileGenerator:
@@ -493,7 +492,10 @@ class DumbProfileJoiner:
)
tool.Geometry.record_object_materials(obj)
if element.is_a("IfcFlowSegment"):
MepGenerator().setup_ports(obj)
# lazy import to avoid circular import errors
from blenderbim.bim.module.model.mep import MEPGenerator
MEPGenerator().setup_ports(obj)
def join(self, profile1, profile2, connection1, connection2, is_relating=True, description="BUTT"):
element1 = tool.Ifc.get_entity(profile1)
@@ -819,7 +821,7 @@ 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()
depth: bpy.props.FloatProperty(subtype="DISTANCE")
@classmethod
def poll(cls, context):
@@ -301,8 +301,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
material = materials[0] # Arbitrarily pick a material
else:
material = self.add_default_material()
named_profiles = [p for p in ifc_file.by_type("IfcProfileDef") if p.ProfileName]
if template == "PROFILESET":
named_profiles = [p for p in ifc_file.by_type("IfcProfileDef") if p.ProfileName]
if named_profiles:
profile = named_profiles[0]
else: