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.SetGableRoofEdgeAngle,
mep.MEPAddObstruction,
mep.MEPAddTransition,
)
addon_keymaps = []
+265 -49
View File
@@ -18,7 +18,10 @@
import bpy
import math
import collections
import bmesh
import re
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.unit
@@ -31,13 +34,13 @@ import blenderbim.core.type
import blenderbim.core.root
import blenderbim.core.geometry
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
import re
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])
float_is_zero = lambda f: 0.0001 >= f >= -0.0001
class RegenerateDistributionElement(bpy.types.Operator, tool.Ifc.Operator):
@@ -86,7 +89,7 @@ class RegenerateDistributionElement(bpy.types.Operator, tool.Ifc.Operator):
def process_branch(branch):
for branch_element in branch:
element = branch_element["element"]
print('processing', element)
print("processing", element)
predecessor = branch_element["predecessor"]
if False: # If the element does not need to be transformed, return early.
return
@@ -107,6 +110,9 @@ class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator):
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 = []
@@ -207,12 +213,7 @@ class MEPGenerator:
ports = tool.System.get_ports(segment)
if segment.is_a("IfcFlowSegment") and not ports:
for mat in [start_port_matrix, end_port_matrix]:
# 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)
tool.System.add_ports(obj)
return
# adjust current segment ports and related flow segments
@@ -248,7 +249,6 @@ class MEPGenerator:
):
if port_position == "start_port":
if segment.is_a("IfcFlowFitting"):
profile_joiner = DumbProfileJoiner()
connected_element_length = (
tool.Model.get_flow_segment_axis(connected_obj)[0]
- tool.Model.get_flow_segment_axis(obj)[0]
@@ -269,45 +269,103 @@ class MEPGenerator:
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,
"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 float_is_zero(port_local_position.length):
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_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):
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, predefined_type):
"""We find compatible fitting only by checking if they were
already used with that segment type before.
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 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 segment_type:
return None
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
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:
if fitting_type.PredefinedType != predefined_type:
continue
@@ -315,13 +373,24 @@ class MEPGenerator:
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
ports = ifcopenshell.util.system.get_ports(fitting)
fitting_data = []
fitting_connected_to_none_type = False
for port in ports:
connected_port = tool.System.get_connected_port(port)
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):
# code is very similar to "bim.add_type"
@@ -333,7 +402,8 @@ class MEPGenerator:
ifc_file = tool.Ifc.get()
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(
tool.Ifc,
tool.Collector,
@@ -377,7 +447,8 @@ class MEPGenerator:
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")
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)
@@ -389,17 +460,11 @@ class MEPGenerator:
obstruction_obj.matrix_world = segment_matrix
profile_joiner.set_depth(obstruction_obj, length)
obstruction = tool.Ifc.get_entity(obstruction_obj)
# TODO: specify PredefinedType based on the segment type
obstruction_port = tool.Ifc.run("system.add_port", element=obstruction)
obstruction_port.PredefinedType = self.get_port_predefined_type(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,
)
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
@@ -411,6 +476,7 @@ class MEPGenerator:
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
@@ -449,3 +515,153 @@ class MEPAddObstruction(bpy.types.Operator, tool.Ifc.Operator):
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)
# 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
if isinstance(x, (list, tuple)):
for y in x:
if value > (y - tolerance) and value < (y + tolerance):
if (y + tolerance) > value > (y - tolerance):
return True
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
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:]]]
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
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.tool as tool
from blenderbim.bim import import_ifc
import re
from mathutils import Matrix
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
def create_empty_at_cursor_with_element_orientation(cls, 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):
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
def import_system_attributes(cls, system):
props = bpy.context.scene.BIMSystemProperties
@@ -19,7 +19,7 @@
import collections
import ifcopenshell
import ifcopenshell.api
from math import cos, sin, pi
from math import cos, sin, pi, tan, radians
from mathutils import Vector, Matrix
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"
)
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)
if position_y_axis:
@@ -579,6 +579,8 @@ class ShapeBuilder:
representation_type = "AdvancedSweptSolid"
elif "IfcExtrudedAreaSolid" in item_types:
representation_type = "SweptSolid"
elif items[0].is_a("IfcTessellatedItem"):
representation_type = "Tessellation"
elif items[0].is_a("IfcCurve") and items[0].Dim == 3:
representation_type = "Curve3D"
else:
@@ -746,8 +748,10 @@ class ShapeBuilder:
ifc_curve = self.file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
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
x2 = SecondFlangeWidth
y = Depth / 2
@@ -770,20 +774,21 @@ class ShapeBuilder:
(-x1+t, -y+t),
(-t/2, -y+t)
)
# fmt: on
# option for no additional thickness in outer radius:
# 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
# )
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),
fillet_radius=(r+t, r+t, r, r, r+t, r+t, r, r),
closed=True, create_ifc_curve=True)
# fmt: on
return ifc_curve
def create_transition_arc_ifc(self, width, height, create_ifc_curve=False):
# create an arc in the rectangle with specified width and height
# if it's not possible to make a complete arc
@@ -814,4 +819,114 @@ class ShapeBuilder:
points, segments, transition_arc = self.get_simple_2dcurve_data(
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