mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 17:58:20 +00:00
Added some MEP related type templates
3 more new types for distribution segments. Distribution segments work the same way as beams - they come without representation but with IfcMaterialProfileSet defined with some profile. Occurences will also have ports at the start and the end of the segment (ports can be displayed in "Ports" section in "Services and Systems"). If you extend the segment or move it around - ports will move accordingly. Small demo - https://imgur.com/a/Aq0utXa
This commit is contained in:
@@ -35,93 +35,31 @@ from mathutils import Vector, Matrix
|
||||
|
||||
|
||||
class MepGenerator:
|
||||
def __init__(self, relating_type):
|
||||
def __init__(self, relating_type=None):
|
||||
self.relating_type = relating_type
|
||||
|
||||
def generate(self):
|
||||
def setup_ports(self, obj):
|
||||
self.file = tool.Ifc.get()
|
||||
self.collection = bpy.context.view_layer.active_layer_collection.collection
|
||||
|
||||
if self.relating_type.is_a("IfcCableCarrierSegmentType"):
|
||||
pass
|
||||
elif self.relating_type.is_a("IfcCableSegmentType"):
|
||||
pass
|
||||
elif self.relating_type.is_a("IfcDuctSegmentType"):
|
||||
dimensions = ifcopenshell.util.element.get_psets(self.relating_type).get("Pset_DuctSegmentTypeCommon") or {}
|
||||
shape = dimensions.get("Shape") or dimensions.get("CrossSectionShape")
|
||||
|
||||
if shape == "RECTANGULAR":
|
||||
self.width = dimensions.get("NominalDiameterOrWidth")
|
||||
self.height = dimensions.get("NominalHeight")
|
||||
self.length = 1
|
||||
|
||||
return self.derive_from_cursor()
|
||||
elif self.relating_type.is_a("IfcPipeSegmentType"):
|
||||
pass
|
||||
|
||||
def derive_from_cursor(self):
|
||||
self.location = bpy.context.scene.cursor.location
|
||||
return self.create_rectangle_segment()
|
||||
|
||||
def create_rectangle_segment(self):
|
||||
verts = [
|
||||
Vector((-self.width / 2, self.height / 2, 0)),
|
||||
Vector((-self.width / 2, -self.height / 2, 0)),
|
||||
Vector((-self.width / 2, self.height / 2, self.length)),
|
||||
Vector((-self.width / 2, -self.height / 2, self.length)),
|
||||
Vector((self.width / 2, self.height / 2, 0)),
|
||||
Vector((self.width / 2, -self.height / 2, 0)),
|
||||
Vector((self.width / 2, self.height / 2, self.length)),
|
||||
Vector((self.width / 2, -self.height / 2, self.length)),
|
||||
]
|
||||
faces = [
|
||||
[1, 3, 2, 0],
|
||||
[4, 6, 7, 5],
|
||||
[1, 0, 4, 5],
|
||||
[3, 7, 6, 2],
|
||||
[0, 2, 6, 4],
|
||||
[1, 5, 7, 3],
|
||||
]
|
||||
mesh = bpy.data.meshes.new(name="Segment")
|
||||
mesh.from_pydata(verts, [], faces)
|
||||
|
||||
ifc_classes = ifcopenshell.util.type.get_applicable_entities(self.relating_type.is_a(), self.file.schema)
|
||||
ifc_class = ifc_classes[0]
|
||||
|
||||
obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(self.relating_type, ifc_class), mesh)
|
||||
|
||||
obj.location = self.location
|
||||
obj.rotation_euler[0] = math.pi / 2
|
||||
obj.rotation_euler[2] = math.pi / 2
|
||||
self.collection.objects.link(obj)
|
||||
|
||||
bpy.ops.bim.assign_class(
|
||||
obj=obj.name,
|
||||
ifc_class=ifc_class,
|
||||
ifc_representation_class="IfcExtrudedAreaSolid/IfcRectangleProfileDef",
|
||||
)
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "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
|
||||
end_port_matrix = Matrix.Translation((0, 0, length))
|
||||
|
||||
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=self.relating_type)
|
||||
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.Mep"})
|
||||
ports = tool.System.get_ports(element)
|
||||
if not ports:
|
||||
start_port_matrix = Matrix()
|
||||
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
|
||||
|
||||
start_port_matrix = Matrix()
|
||||
end_port_matrix = Matrix.Translation((0, 0, self.length))
|
||||
|
||||
for mat in [start_port_matrix, end_port_matrix]:
|
||||
port = tool.Ifc.run("root.create_entity", ifc_class="IfcDistributionPort")
|
||||
tool.Ifc.run("system.assign_port", element=element, port=port)
|
||||
tool.Ifc.run("geometry.edit_object_placement", product=port, matrix=obj.matrix_world @ mat, is_si=True)
|
||||
|
||||
try:
|
||||
obj.select_set(True)
|
||||
except RuntimeError:
|
||||
def msg(self, context):
|
||||
txt = "The created object could not be assigned to a collection. "
|
||||
txt += "Has any IfcSpatialElement been deleted?"
|
||||
self.layout.label(text=txt)
|
||||
|
||||
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||
return obj
|
||||
# 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
|
||||
tool.Model.edit_element_placement(end_port, obj.matrix_world @ end_port_matrix)
|
||||
|
||||
@@ -105,15 +105,16 @@ class AddConstrTypeInstance(bpy.types.Operator):
|
||||
material = ifcopenshell.util.element.get_material(relating_type)
|
||||
|
||||
if material and material.is_a("IfcMaterialProfileSet"):
|
||||
if profile.DumbProfileGenerator(relating_type).generate():
|
||||
if obj := profile.DumbProfileGenerator(relating_type).generate():
|
||||
if not relating_type.is_a("IfcFlowSegmentType"):
|
||||
return {"FINISHED"}
|
||||
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):
|
||||
tool.Blender.select_and_activate_single_object(context, context.selected_objects[-1])
|
||||
return {"FINISHED"}
|
||||
if relating_type.is_a("IfcFlowSegmentType") and not relating_type.RepresentationMaps:
|
||||
if mep.MepGenerator(relating_type).generate():
|
||||
return {"FINISHED"}
|
||||
|
||||
building_obj = None
|
||||
if len(context.selected_objects) == 1 and context.active_object:
|
||||
|
||||
@@ -34,6 +34,7 @@ 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:
|
||||
@@ -74,7 +75,9 @@ class DumbProfileGenerator:
|
||||
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"]:
|
||||
if self.relating_type.is_a() in ["IfcBeamType", "IfcMemberType"] or self.relating_type.is_a(
|
||||
"IfcFlowSegmentType"
|
||||
):
|
||||
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 self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
|
||||
@@ -230,9 +233,11 @@ class ExtendProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
for obj in selected_objs:
|
||||
tool.Geometry.clear_scale(obj)
|
||||
|
||||
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)
|
||||
@@ -245,6 +250,7 @@ class ExtendProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if obj == context.active_object:
|
||||
continue
|
||||
joiner.join_T(obj, context.active_object)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -486,6 +492,8 @@ class DumbProfileJoiner:
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
tool.Geometry.record_object_materials(obj)
|
||||
if element.is_a("IfcFlowSegment"):
|
||||
MepGenerator().setup_ports(obj)
|
||||
|
||||
def join(self, profile1, profile2, connection1, connection2, is_relating=True, description="BUTT"):
|
||||
element1 = tool.Ifc.get_entity(profile1)
|
||||
@@ -869,9 +877,15 @@ class Rotate90(bpy.types.Operator, tool.Ifc.Operator):
|
||||
elif usage == "LAYER2":
|
||||
layer2_objs.append(obj)
|
||||
if element.ConnectedTo or element.ConnectedFrom:
|
||||
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATSTART")
|
||||
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATEND")
|
||||
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATPATH")
|
||||
ifcopenshell.api.run(
|
||||
"geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATSTART"
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
"geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATEND"
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
"geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATPATH"
|
||||
)
|
||||
rotate_matrix = Matrix.Rotation(pi / 2, 4, self.axis)
|
||||
obj.matrix_world @= rotate_matrix
|
||||
bpy.context.view_layer.update()
|
||||
@@ -992,7 +1006,7 @@ class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
|
||||
y_axis = Vector((0, 0, 1))
|
||||
# making sure z_axis != y_axis
|
||||
if z_axis == y_axis:
|
||||
y_axis = Vector((0,1,0))
|
||||
y_axis = Vector((0, 1, 0))
|
||||
|
||||
x_axis = y_axis.cross(z_axis).normalized()
|
||||
y_axis = z_axis.cross(x_axis).normalized()
|
||||
|
||||
@@ -147,7 +147,9 @@ class BIMModelProperties(PropertyGroup):
|
||||
("STAIR", "Stair", "Parametric stair"),
|
||||
("RAILING", "Railing", "Parametric railing"),
|
||||
("ROOF", "Roof", "Parametric roof"),
|
||||
|
||||
("DISTRIBUTION_SEGMENT_RECTANGULAR", "Rectangular Distribution Segment", "Works similarly to Profile, has distribution ports"),
|
||||
("DISTRIBUTION_SEGMENT_CIRCULAR", "Circular Distribution Segment", "Works similarly to Profile, has distribution ports"),
|
||||
("DISTRIBUTION_SEGMENT_CIRCULAR_HOLLOW", "Circular Hollow Distribution Segment", "Works similarly to Profile, has distribution ports"),
|
||||
),
|
||||
name="Type Template",
|
||||
default="MESH",
|
||||
|
||||
@@ -278,7 +278,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
axis = "AXIS3"
|
||||
ifcopenshell.api.run("pset.edit_pset", ifc_file, pset=pset, properties={"LayerSetDirection": axis})
|
||||
|
||||
elif template == "PROFILESET":
|
||||
elif template == "PROFILESET" or template.startswith("DISTRIBUTION_SEGMENT_"):
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
obj = bpy.data.objects.new(name, None)
|
||||
element = blenderbim.core.root.assign_class(
|
||||
@@ -298,13 +298,45 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
else:
|
||||
material = self.add_default_material()
|
||||
named_profiles = [p for p in ifc_file.by_type("IfcProfileDef") if p.ProfileName]
|
||||
if named_profiles:
|
||||
profile = named_profiles[0]
|
||||
if template == "PROFILESET":
|
||||
if named_profiles:
|
||||
profile = named_profiles[0]
|
||||
else:
|
||||
size = 0.5 / unit_scale
|
||||
profile = ifc_file.create_entity(
|
||||
"IfcRectangleProfileDef", ProfileName="New Profile", ProfileType="AREA", XDim=size, YDim=size
|
||||
)
|
||||
else:
|
||||
size = 0.5 / unit_scale
|
||||
profile = ifc_file.create_entity(
|
||||
"IfcRectangleProfileDef", ProfileName="New Profile", ProfileType="AREA", XDim=size, YDim=size
|
||||
)
|
||||
# NOTE: defaults dims are in meters / mm
|
||||
if template == "DISTRIBUTION_SEGMENT_RECTANGULAR":
|
||||
default_x_dim = 0.4 / unit_scale
|
||||
default_y_dim = 0.2 / unit_scale
|
||||
profile_name = f"{ifc_class}-{default_x_dim*1000}x{default_x_dim*1000}"
|
||||
profile = ifc_file.create_entity(
|
||||
"IfcRectangleProfileDef",
|
||||
ProfileName=profile_name,
|
||||
ProfileType="AREA",
|
||||
XDim=default_x_dim,
|
||||
YDim=default_y_dim,
|
||||
)
|
||||
elif template == "DISTRIBUTION_SEGMENT_CIRCULAR":
|
||||
default_diameter = 0.1 / unit_scale
|
||||
profile_name = f"{ifc_class}-{default_diameter*1000}"
|
||||
profile = ifc_file.create_entity(
|
||||
"IfcCircleProfileDef", ProfileName=profile_name, ProfileType="AREA", Radius=default_diameter / 2
|
||||
)
|
||||
elif template == "DISTRIBUTION_SEGMENT_CIRCULAR_HOLLOW":
|
||||
default_diameter = 0.15 / unit_scale
|
||||
default_thickness = 0.005 / unit_scale # TODO: double check
|
||||
profile_name = f"{ifc_class}-{default_diameter*1000}x{default_thickness*1000}"
|
||||
profile = ifc_file.create_entity(
|
||||
"IfcCircleHollowProfileDef",
|
||||
ProfileName=profile_name,
|
||||
ProfileType="AREA",
|
||||
Radius=default_diameter / 2,
|
||||
WallThickness=default_thickness,
|
||||
)
|
||||
|
||||
rel = ifcopenshell.api.run(
|
||||
"material.assign_material", ifc_file, product=element, type="IfcMaterialProfileSet"
|
||||
)
|
||||
|
||||
@@ -748,3 +748,13 @@ class Model(blenderbim.core.tool.Model):
|
||||
return
|
||||
pset_data["data_dict"] = json.loads(pset_data.get("Data", "[]") or "[]")
|
||||
return pset_data
|
||||
|
||||
@classmethod
|
||||
def edit_element_placement(cls, element, matrix):
|
||||
"""Useful for moving objects like ports or openings -
|
||||
the method will ensure it will be moved in blender scene too if it exists"""
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
obj.matrix_world = matrix
|
||||
return
|
||||
tool.Ifc.run("geometry.edit_object_placement", product=element, matrix=matrix, is_si=True)
|
||||
|
||||
Reference in New Issue
Block a user