bim.add_transition

Added simple operator to add transition between two mep segments (now only rectangular collinear segments are supported).
It also reuses the transition type that was previously used to connect segments of the same type.

Demonstration - https://imgur.com/a/c1AOxj1
This commit is contained in:
Andrej730
2023-08-04 14:46:48 +05:00
parent 4dc55e844c
commit 2c1c13f27e
5 changed files with 471 additions and 59 deletions
@@ -177,6 +177,7 @@ classes = (
roof.RemoveRoof, roof.RemoveRoof,
roof.SetGableRoofEdgeAngle, roof.SetGableRoofEdgeAngle,
mep.MEPAddObstruction, mep.MEPAddObstruction,
mep.MEPAddTransition,
) )
addon_keymaps = [] addon_keymaps = []
+265 -49
View File
@@ -18,7 +18,10 @@
import bpy import bpy
import math import math
import collections
import bmesh import bmesh
import re
import json
import ifcopenshell import ifcopenshell
import ifcopenshell.api import ifcopenshell.api
import ifcopenshell.util.unit import ifcopenshell.util.unit
@@ -31,13 +34,13 @@ import blenderbim.core.type
import blenderbim.core.root import blenderbim.core.root
import blenderbim.core.geometry import blenderbim.core.geometry
import blenderbim.tool as tool import blenderbim.tool as tool
from math import pi, degrees from math import pi, degrees, radians
from copy import copy
from mathutils import Vector, Matrix from mathutils import Vector, Matrix
import re from ifcopenshell.util.shape_builder import ShapeBuilder
from blenderbim.bim.module.model.profile import DumbProfileJoiner from blenderbim.bim.module.model.profile import DumbProfileJoiner
V = lambda *x: Vector([float(i) for i in x]) V = lambda *x: Vector([float(i) for i in x])
float_is_zero = lambda f: 0.0001 >= f >= -0.0001
class RegenerateDistributionElement(bpy.types.Operator, tool.Ifc.Operator): class RegenerateDistributionElement(bpy.types.Operator, tool.Ifc.Operator):
@@ -86,7 +89,7 @@ class RegenerateDistributionElement(bpy.types.Operator, tool.Ifc.Operator):
def process_branch(branch): def process_branch(branch):
for branch_element in branch: for branch_element in branch:
element = branch_element["element"] element = branch_element["element"]
print('processing', element) print("processing", element)
predecessor = branch_element["predecessor"] predecessor = branch_element["predecessor"]
if False: # If the element does not need to be transformed, return early. if False: # If the element does not need to be transformed, return early.
return return
@@ -107,6 +110,9 @@ class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def _execute(self, context): def _execute(self, context):
# TODO: need to add ui for parameters:
# - obstruction cap thickness
# - start/end thickness and angle for transition
selected_objs = [] selected_objs = []
selected_profiles = [] selected_profiles = []
@@ -207,12 +213,7 @@ class MEPGenerator:
ports = tool.System.get_ports(segment) ports = tool.System.get_ports(segment)
if segment.is_a("IfcFlowSegment") and not ports: if segment.is_a("IfcFlowSegment") and not ports:
for mat in [start_port_matrix, end_port_matrix]: tool.System.add_ports(obj)
# TODO: specify PredefinedType based on the segment type
port = tool.Ifc.run("system.add_port", element=segment)
port.FlowDirection = "NOTDEFINED"
port.PredefinedType = self.get_port_predefined_type(segment)
tool.Ifc.run("geometry.edit_object_placement", product=port, matrix=mat, is_si=True)
return return
# adjust current segment ports and related flow segments # adjust current segment ports and related flow segments
@@ -248,7 +249,6 @@ class MEPGenerator:
): ):
if port_position == "start_port": if port_position == "start_port":
if segment.is_a("IfcFlowFitting"): if segment.is_a("IfcFlowFitting"):
profile_joiner = DumbProfileJoiner()
connected_element_length = ( connected_element_length = (
tool.Model.get_flow_segment_axis(connected_obj)[0] tool.Model.get_flow_segment_axis(connected_obj)[0]
- tool.Model.get_flow_segment_axis(obj)[0] - tool.Model.get_flow_segment_axis(obj)[0]
@@ -269,45 +269,103 @@ class MEPGenerator:
extrusion_depth = segment_object.dimensions.z extrusion_depth = segment_object.dimensions.z
end_point = segment_object.matrix_world @ V(0, 0, extrusion_depth) end_point = segment_object.matrix_world @ V(0, 0, extrusion_depth)
segment_data = { segment_data = {
"start_point": start_point, "start_point": start_point.copy().freeze(),
"end_point": end_point, "end_point": end_point.freeze(),
"ports": ports, "ports": ports,
"extrusion_depth": extrusion_depth, "extrusion_depth": extrusion_depth,
} }
for port in ports: for port in ports:
port_local_position = V(*port.ObjectPlacement.RelativePlacement.Location.Coordinates) port_local_position = V(*port.ObjectPlacement.RelativePlacement.Location.Coordinates)
if float_is_zero(port_local_position.length): if tool.Cad.is_x(port_local_position.length, 0.0):
segment_data["start_port"] = port segment_data["start_port"] = port
else: else:
segment_data["end_port"] = port segment_data["end_port"] = port
return segment_data return segment_data
def get_port_predefined_type(self, segment):
split_camel_case = lambda x: re.findall("[A-Z][^A-Z]*", x)
class_name = "".join(split_camel_case(segment.is_a())[1:-1]).upper()
if class_name == "CONVEYOR":
return "NOTDEFINED"
return class_name
def get_mep_element_class_name(self, element, mep_class_type): def get_mep_element_class_name(self, element, mep_class_type):
split_camel_case = lambda x: re.findall("[A-Z][^A-Z]*", x) 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]) class_name = "".join(split_camel_case(element.is_a())[:-1] + [mep_class_type])
return class_name return class_name
def get_compatible_fitting_type(self, segment, predefined_type): def get_compatible_fitting_type(self, segment_or_segments, port_or_ports, predefined_type):
"""We find compatible fitting only by checking if they were """
already used with that segment type before. 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 There lies the problem that it won't be
able to identify the fittings that were not connected to any segments yet. able to identify the fittings that were not yet connected to any segments yet.
""" """
segment_type = ifcopenshell.util.element.get_type(segment) if not isinstance(segment_or_segments, collections.abc.Iterable):
if not segment_type: segments = [segment_or_segments]
return None ports = [port_or_ports]
else:
segments = segment_or_segments
ports = port_or_ports
fitting_types = tool.Ifc.get().by_type(self.get_mep_element_class_name(segment, "Fitting")) 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 occurences
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: for fitting_type in fitting_types:
if fitting_type.PredefinedType != predefined_type: if fitting_type.PredefinedType != predefined_type:
continue continue
@@ -315,13 +373,24 @@ class MEPGenerator:
if not fittings: if not fittings:
continue continue
fitting = fittings[0] fitting = fittings[0]
elements = set(
ifcopenshell.util.system.get_connected_to(fitting) ports = ifcopenshell.util.system.get_ports(fitting)
+ ifcopenshell.util.system.get_connected_from(fitting) fitting_data = []
) fitting_connected_to_none_type = False
for element in elements: for port in ports:
if element.IsTypedBy and element.IsTypedBy[0].RelatingType == segment_type: connected_port = tool.System.get_connected_port(port)
return fitting_type connected_element = tool.System.get_port_relating_element(connected_port)
element_type = ifcopenshell.util.element.get_type(connected_element)
if element_type is None:
fitting_connected_to_none_type = True
break
fitting_data.append((element_type, port.PredefinedType, port.SystemType))
if fitting_connected_to_none_type:
continue
if are_connected_elements_compatible(segments_data, fitting_data):
return pack_return_data(fitting_type, ports, segments_data)
def create_obstruction_type(self, segment): def create_obstruction_type(self, segment):
# code is very similar to "bim.add_type" # code is very similar to "bim.add_type"
@@ -333,7 +402,8 @@ class MEPGenerator:
ifc_file = tool.Ifc.get() ifc_file = tool.Ifc.get()
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
obj = bpy.data.objects.new("Fitting", None) obj = bpy.data.objects.new("Obstruction", None)
# TODO: OBSTRUCTION predefined type is available only for IfcDuctFitting and IfcPipeFitting
element = blenderbim.core.root.assign_class( element = blenderbim.core.root.assign_class(
tool.Ifc, tool.Ifc,
tool.Collector, tool.Collector,
@@ -377,7 +447,8 @@ class MEPGenerator:
segment_obj = tool.Ifc.get_object(segment) segment_obj = tool.Ifc.get_object(segment)
segment_matrix = segment_obj.matrix_world segment_matrix = segment_obj.matrix_world
segment_rotation = segment_matrix.to_quaternion() segment_rotation = segment_matrix.to_quaternion()
obstruction_type = self.get_compatible_fitting_type(segment, "OBSTRUCTION") 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: if not obstruction_type:
obstruction_type = self.create_obstruction_type(segment) obstruction_type = self.create_obstruction_type(segment)
@@ -389,17 +460,11 @@ class MEPGenerator:
obstruction_obj.matrix_world = segment_matrix obstruction_obj.matrix_world = segment_matrix
profile_joiner.set_depth(obstruction_obj, length) profile_joiner.set_depth(obstruction_obj, length)
obstruction = tool.Ifc.get_entity(obstruction_obj) obstruction_port = tool.System.add_ports(
# TODO: specify PredefinedType based on the segment type obstruction_obj,
obstruction_port = tool.Ifc.run("system.add_port", element=obstruction) add_start_port=not at_segment_start,
obstruction_port.PredefinedType = self.get_port_predefined_type(obstruction) add_end_port=at_segment_start,
port_local_position = Matrix.Translation((0, 0, length)) if at_segment_start else Matrix() )[0]
tool.Ifc.run(
"geometry.edit_object_placement",
product=obstruction_port,
matrix=segment_matrix @ port_local_position,
is_si=True,
)
# change segment length # change segment length
new_segment_length = segment_data["extrusion_depth"] - length new_segment_length = segment_data["extrusion_depth"] - length
@@ -411,6 +476,7 @@ class MEPGenerator:
obstruction_obj.location += segment_rotation @ V(0, 0, new_segment_length) 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") tool.Ifc.run("system.connect_port", port1=related_port, port2=obstruction_port, direction="NOTDEFINED")
obstruction = tool.Ifc.get_entity(obstruction_obj)
return obstruction, None return obstruction, None
@@ -449,3 +515,153 @@ class MEPAddObstruction(bpy.types.Operator, tool.Ifc.Operator):
return {"CANCELLED"} return {"CANCELLED"}
return {"FINISHED"} 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)
# TODO: support cases when segments are partially or completely overlapping each other
if not tool.Cad.are_edges_collinear(start_axis, end_axis):
self.report({"ERROR"}, f"Failed to add transition - non collinear segments are not yet supported.")
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"],
}
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"]),
)
transition_dir = (end_point - start_point).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
)
if not rep:
self.report({"ERROR"}, f"Failed to add transition - this kind of profiles is not yet supported.")
return {"CANCELLED"}
middle_point = (start_point + end_point) / 2
full_transition_length = transition_data["full_transition_length"] * si_conversion
start_segment_extend_point = middle_point - transition_dir * full_transition_length / 2
end_segment_extend_point = middle_point + transition_dir * full_transition_length / 2
DumbProfileJoiner().join_E(start_object, start_segment_extend_point)
DumbProfileJoiner().join_E(end_object, end_segment_extend_point)
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
start_port_match = fitting_data["start_port_match"] if fitting_data else True
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
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
transition_obj.matrix_world = start_object.matrix_world
context.view_layer.update()
transition_obj_dir = tool.Cad.get_edge_direction(tool.Model.get_flow_segment_axis(transition_obj))
direction_match = tool.Cad.are_vectors_equal(transition_obj_dir, transition_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)
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"}
+48 -2
View File
@@ -91,10 +91,14 @@ class Cad:
tolerance = VTX_PRECISION tolerance = VTX_PRECISION
if isinstance(x, (list, tuple)): if isinstance(x, (list, tuple)):
for y in x: for y in x:
if value > (y - tolerance) and value < (y + tolerance): if (y + tolerance) > value > (y - tolerance):
return True return True
return False return False
return value > (x - tolerance) and value < (x + tolerance) return (x + tolerance) > value > (x - tolerance)
@classmethod
def are_vectors_equal(cls, v1: Vector, v2: Vector):
return cls.is_x((v2 - v1).length, 0)
@classmethod @classmethod
def intersect_edges(cls, edge1, edge2): def intersect_edges(cls, edge1, edge2):
@@ -227,6 +231,48 @@ class Cad:
res = [cls.is_point_on_edge(pt, edge) for edge in [edges[:2], edges[2:]]] res = [cls.is_point_on_edge(pt, edge) for edge in [edges[:2], edges[2:]]]
return len([i for i in res if i]) return len([i for i in res if i])
@classmethod
def get_edge_direction(cls, edge):
return (edge[1] - edge[0]).normalized()
@classmethod
def are_edges_collinear(cls, edge1, edge2):
def is_point_on_line(p, edge):
a1, a2 = edge
# comparing slopes between PA1 and A2A1
# using cross multiplication to avoid division by zero
return cls.is_x((p.y - a1.y) * (a2.x - a1.x), (a2.y - a1.y) * (p.x - a1.x))
edge1_dir = edge1[1] - edge1[0]
edge2_dir = edge2[1] - edge2[0]
if cls.is_x(edge1_dir.cross(edge2_dir).length_squared, 0): # check they are parallel
if is_point_on_line(edge1[0], edge2) or is_point_on_line(edge1[1], edge2):
return True
return False
@classmethod
def closest_points(cls, edge1, edge2):
"""
closest end points between `edge1` and `edge2` assuming `edge1` and `edge2` are collinear.
< returns two points, first one belongs to `edge1` and second to `edge2`
"""
direction = (edge1[1] - edge1[0]).normalized()
# Project points onto the line to get scalar values along the direction
points1_values = [(p, p.dot(direction)) for p in edge1]
points2_values = [(p, p.dot(direction)) for p in edge2]
# Sort the projections for both edges
sorted_points1 = sorted(points1_values, key=lambda el: el[1])
sorted_points2 = sorted(points2_values, key=lambda el: el[1])
# The closest points will be the last point of the first edge and the first point of the second edge
return sorted_points1[-1][0], sorted_points2[0][0]
@classmethod @classmethod
def find_intersecting_edges(cls, bm, pt, idx1, idx2): def find_intersecting_edges(cls, bm, pt, idx1, idx2):
""" """
+34
View File
@@ -21,9 +21,35 @@ import ifcopenshell.util.system
import blenderbim.core.tool import blenderbim.core.tool
import blenderbim.tool as tool import blenderbim.tool as tool
from blenderbim.bim import import_ifc from blenderbim.bim import import_ifc
import re
from mathutils import Matrix
class System(blenderbim.core.tool.System): class System(blenderbim.core.tool.System):
@classmethod
def add_ports(cls, obj, add_start_port=True, add_end_port=True):
def add_port(mep_element, matrix):
port = tool.Ifc.run("system.add_port", element=mep_element)
port.FlowDirection = "NOTDEFINED"
port.PredefinedType = tool.System.get_port_predefined_type(mep_element)
tool.Ifc.run("geometry.edit_object_placement", product=port, matrix=matrix, is_si=True)
return port
# make sure obj.dimensions and .matrix_world has valid data
bpy.context.view_layer.update()
# need to make sure .ObjectPlacement is also updated when we're going to add ports
if tool.Ifc.is_moved(obj):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
mep_element = tool.Ifc.get_entity(obj)
length = obj.dimensions.z
ports = []
if add_start_port:
ports.append(add_port(mep_element, obj.matrix_world @ Matrix()))
if add_end_port:
ports.append(add_port(mep_element, obj.matrix_world @ Matrix.Translation((0, 0, length))))
return ports
@classmethod @classmethod
def create_empty_at_cursor_with_element_orientation(cls, element): def create_empty_at_cursor_with_element_orientation(cls, element):
element_obj = tool.Ifc.get_object(element) element_obj = tool.Ifc.get_object(element)
@@ -68,6 +94,14 @@ class System(blenderbim.core.tool.System):
def get_port_relating_element(cls, port): def get_port_relating_element(cls, port):
return port.Nests[0].RelatingObject return port.Nests[0].RelatingObject
@classmethod
def get_port_predefined_type(cls, mep_element):
split_camel_case = lambda x: re.findall("[A-Z][^A-Z]*", x)
class_name = "".join(split_camel_case(mep_element.is_a())[1:-1]).upper()
if class_name == "CONVEYOR":
return "NOTDEFINED"
return class_name
@classmethod @classmethod
def import_system_attributes(cls, system): def import_system_attributes(cls, system):
props = bpy.context.scene.BIMSystemProperties props = bpy.context.scene.BIMSystemProperties
@@ -19,7 +19,7 @@
import collections import collections
import ifcopenshell import ifcopenshell
import ifcopenshell.api import ifcopenshell.api
from math import cos, sin, pi from math import cos, sin, pi, tan, radians
from mathutils import Vector, Matrix from mathutils import Vector, Matrix
from itertools import chain from itertools import chain
@@ -539,7 +539,7 @@ class ShapeBuilder:
"Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcPositiveLengthMeasure.htm#8.11.2.71.3-Formal-representation" "Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcPositiveLengthMeasure.htm#8.11.2.71.3-Formal-representation"
) )
if profile_or_curve.is_a() not in ("IfcArbitraryClosedProfileDef", "IfcArbitraryProfileDefWithVoids"): if not profile_or_curve.is_a("IfcProfileDef"):
profile_or_curve = self.profile(profile_or_curve) profile_or_curve = self.profile(profile_or_curve)
if position_y_axis: if position_y_axis:
@@ -579,6 +579,8 @@ class ShapeBuilder:
representation_type = "AdvancedSweptSolid" representation_type = "AdvancedSweptSolid"
elif "IfcExtrudedAreaSolid" in item_types: elif "IfcExtrudedAreaSolid" in item_types:
representation_type = "SweptSolid" representation_type = "SweptSolid"
elif items[0].is_a("IfcTessellatedItem"):
representation_type = "Tessellation"
elif items[0].is_a("IfcCurve") and items[0].Dim == 3: elif items[0].is_a("IfcCurve") and items[0].Dim == 3:
representation_type = "Curve3D" representation_type = "Curve3D"
else: else:
@@ -747,7 +749,9 @@ class ShapeBuilder:
ifc_curve = self.file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments) ifc_curve = self.file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
return (points, segments, ifc_curve) return (points, segments, ifc_curve)
def create_z_profile_lips_curve(self, FirstFlangeWidth, SecondFlangeWidth, Depth, Girth, WallThickness, FilletRadius): def create_z_profile_lips_curve(
self, FirstFlangeWidth, SecondFlangeWidth, Depth, Girth, WallThickness, FilletRadius
):
x1 = FirstFlangeWidth x1 = FirstFlangeWidth
x2 = SecondFlangeWidth x2 = SecondFlangeWidth
y = Depth / 2 y = Depth / 2
@@ -770,17 +774,18 @@ class ShapeBuilder:
(-x1+t, -y+t), (-x1+t, -y+t),
(-t/2, -y+t) (-t/2, -y+t)
) )
# fmt: on
# option for no additional thickness in outer radius: # option for no additional thickness in outer radius:
# points, segments, ifc_curve = create_curve_from_coords( # points, segments, ifc_curve = create_curve_from_coords(
# coords, fillets = (0, 1, 4, 5, 6, 7, 10, 11), fillet_radius=r, closed=True, ifc_file=ifc_file # coords, fillets = (0, 1, 4, 5, 6, 7, 10, 11), fillet_radius=r, closed=True, ifc_file=ifc_file
# ) # )
points, segments, ifc_curve = self.get_simple_2dcurve_data(coords, points, segments, ifc_curve = self.get_simple_2dcurve_data(
coords,
fillets = (0, 1, 4, 5, 6, 7, 10, 11), fillets = (0, 1, 4, 5, 6, 7, 10, 11),
fillet_radius=(r+t, r+t, r, r, r+t, r+t, r, r), fillet_radius=(r+t, r+t, r, r, r+t, r+t, r, r),
closed=True, create_ifc_curve=True) closed=True, create_ifc_curve=True)
# fmt: on
return ifc_curve return ifc_curve
@@ -815,3 +820,113 @@ class ShapeBuilder:
curve_coords, fillets, fillet_radius, closed=False, create_ifc_curve=create_ifc_curve curve_coords, fillets, fillet_radius, closed=False, create_ifc_curve=create_ifc_curve
) )
return points, segments, transition_arc return points, segments, transition_arc
def polygonal_face_set(self, points, faces):
"""
> `points` - list of points
> `faces` - list of faces consisted of point indices (points indices starting from 0)
< IfcPolygonalFaceSet
"""
ifc_points = self.file.createIfcCartesianPointList3D(points)
ifc_faces = []
for face in faces:
face = [i + 1 for i in face]
ifc_faces.append(self.file.createIfcIndexedPolygonalFace(face))
face_set = self.file.createIfcPolygonalFaceSet(Coordinates=ifc_points, Faces=ifc_faces)
return face_set
def mep_transition_shape(self, start_segment, end_segment, start_length, end_length, angle=30.0):
"""
returns tuple of Model/Body/MODEL_VIEW IfcRepresentation and transition shape data
"""
# good default values from angle = 30/60 deg
# 30 degree angle will result in 75 degrees on the transition (= 90 - α/2) - https://i.imgur.com/tcoYDWu.png
# TODO: get rid of reliance on profiles
def get_profile(element):
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
if material and material.is_a("IfcMaterialProfileSet") and len(material.MaterialProfiles) == 1:
return material.MaterialProfiles[0].Profile
start_profile = get_profile(start_segment)
end_profile = get_profile(end_segment)
# TODO: support more profiles
if not start_profile.is_a("IfcRectangleProfileDef") or not end_profile.is_a("IfcRectangleProfileDef"):
# Non rectangular profiles are not yet supported
return None, None
start_half_dim = V(start_profile.XDim / 2, start_profile.YDim / 2, start_length)
end_half_dim = V(end_profile.XDim / 2, end_profile.YDim / 2, end_length)
transition_items = []
end_extrusion_offset = V(0, 0, start_length)
def get_transition_legth(start_half_dim, end_half_dim, angle):
diff = start_half_dim.xy - end_half_dim.xy
diff = Vector([abs(i) for i in diff])
c = diff.x * tan(radians(90 - angle / 2))
a = diff.y
b = (c**2 - a**2) ** 0.5
return b
transition_length = get_transition_legth(start_half_dim, end_half_dim, angle)
faces = []
if transition_length != 0:
end_extrusion_offset.z += transition_length
faces += [(3, 4, 7, 0), (11, 8, 15, 12), (3, 11, 12, 4), (7, 15, 8, 0)]
# NOTE: clockwise order for correct face orientation
faces += [
# start extrusion
(0, 1, 2, 3),
(8, 11, 10, 9),
(0, 8, 9, 1),
(1, 9, 10, 2),
(2, 10, 11, 3),
# end extrusion
(4, 5, 6, 7),
(12, 15, 14, 13),
(4, 12, 13, 5),
(5, 13, 14, 6),
(6, 14, 15, 7),
]
points = [
start_half_dim * V(-1, -1, 1),
start_half_dim * V(-1, -1, 0),
start_half_dim * V(1, -1, 0),
start_half_dim * V(1, -1, 1),
end_half_dim * V(1, -1, 0) + end_extrusion_offset,
end_half_dim * V(1, -1, 1) + end_extrusion_offset,
end_half_dim * V(-1, -1, 1) + end_extrusion_offset,
end_half_dim * V(-1, -1, 0) + end_extrusion_offset,
start_half_dim * V(-1, 1, 1),
start_half_dim * V(-1, 1, 0),
start_half_dim * V(1, 1, 0),
start_half_dim * V(1, 1, 1),
end_half_dim * V(1, 1, 0) + end_extrusion_offset,
end_half_dim * V(1, 1, 1) + end_extrusion_offset,
end_half_dim * V(-1, 1, 1) + end_extrusion_offset,
end_half_dim * V(-1, 1, 0) + end_extrusion_offset,
]
face_set = self.polygonal_face_set(points, faces)
transition_items.append(face_set)
body = ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW")
representation = self.get_representation(body, transition_items, "Tesselation")
transition_data = {
"start_length": start_length,
"end_length": end_length,
"angle": angle,
"transition_length": transition_length,
"full_transition_length": start_length + transition_length + end_length,
}
return representation, transition_data