mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-20 06:58:56 +00:00
Considering parametric data for fitting #3695
Search for compatible fitting wasn't taking into account that maybe fitting used for the same segments but fitting's parameters are not the same (such as start_length, end_length, angle and offset between profiles).
I also forgot to promote transition angle to operator's property😬
This commit is contained in:
@@ -40,6 +40,7 @@ from copy import copy
|
|||||||
from mathutils import Vector, Matrix
|
from mathutils import Vector, Matrix
|
||||||
from ifcopenshell.util.shape_builder import ShapeBuilder
|
from ifcopenshell.util.shape_builder import ShapeBuilder
|
||||||
from blenderbim.bim.module.model.profile import DumbProfileJoiner
|
from blenderbim.bim.module.model.profile import DumbProfileJoiner
|
||||||
|
from blenderbim.tool.cad import VTX_PRECISION
|
||||||
|
|
||||||
V = lambda *x: Vector([float(i) for i in x])
|
V = lambda *x: Vector([float(i) for i in x])
|
||||||
|
|
||||||
@@ -47,7 +48,8 @@ V = lambda *x: Vector([float(i) for i in x])
|
|||||||
class RegenerateDistributionElement(bpy.types.Operator, tool.Ifc.Operator):
|
class RegenerateDistributionElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
bl_idname = "bim.regenerate_distribution_element"
|
bl_idname = "bim.regenerate_distribution_element"
|
||||||
bl_description = (
|
bl_description = (
|
||||||
"Regenerates the positions and segment lengths of a distribution element and all connected elements."
|
"Regenerates the positions and segment lengths of a distribution element and all connected elements.\n"
|
||||||
|
"Will try to adjust as less elements as possible, never rotate them. Segments will also try to change their length to fit"
|
||||||
)
|
)
|
||||||
bl_label = "Regenerate Distribution Element"
|
bl_label = "Regenerate Distribution Element"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
@@ -335,7 +337,7 @@ class MEPGenerator:
|
|||||||
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_or_segments, port_or_ports, predefined_type):
|
def get_compatible_fitting_type(self, segment_or_segments, port_or_ports, predefined_type, bbim_data=None):
|
||||||
"""
|
"""
|
||||||
returns a dict of compatible fitting_type and start_port_match flag to correctly place the fitting.
|
returns a dict of compatible fitting_type and start_port_match flag to correctly place the fitting.
|
||||||
|
|
||||||
@@ -348,9 +350,12 @@ class MEPGenerator:
|
|||||||
|
|
||||||
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 yet connected to any segments yet.
|
able to identify the fittings that were not yet connected to any segments yet.
|
||||||
|
|
||||||
|
|
||||||
|
`bbim_data` is used to find compatible fitting build with BBIM parametrically (BBIM_Fitting pset).
|
||||||
|
All data in `bbim_data` supposed to be in project units.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# TODO: check angle, start, end and offset for transitions
|
|
||||||
if not isinstance(segment_or_segments, collections.abc.Iterable):
|
if not isinstance(segment_or_segments, collections.abc.Iterable):
|
||||||
segments = [segment_or_segments]
|
segments = [segment_or_segments]
|
||||||
ports = [port_or_ports]
|
ports = [port_or_ports]
|
||||||
@@ -358,6 +363,10 @@ class MEPGenerator:
|
|||||||
segments = segment_or_segments
|
segments = segment_or_segments
|
||||||
ports = port_or_ports
|
ports = port_or_ports
|
||||||
|
|
||||||
|
ifc_file = tool.Ifc.get()
|
||||||
|
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||||
|
precision = VTX_PRECISION / si_conversion
|
||||||
|
|
||||||
segments_data = []
|
segments_data = []
|
||||||
for segment, port in zip(segments, ports, strict=True):
|
for segment, port in zip(segments, ports, strict=True):
|
||||||
segment_type = ifcopenshell.util.element.get_type(segment)
|
segment_type = ifcopenshell.util.element.get_type(segment)
|
||||||
@@ -366,6 +375,28 @@ class MEPGenerator:
|
|||||||
return
|
return
|
||||||
segments_data.append((segment_type, port.PredefinedType, port.SystemType))
|
segments_data.append((segment_type, port.PredefinedType, port.SystemType))
|
||||||
|
|
||||||
|
# TODO: test it with flipped transition where start length != end length
|
||||||
|
def compatible_with_bbim_data(fitting_type):
|
||||||
|
if not bbim_data:
|
||||||
|
return True
|
||||||
|
fitting_type_obj = tool.Ifc.get_object(fitting_type)
|
||||||
|
fitting_bbim_data = tool.Model.get_modeling_bbim_pset_data(fitting_type_obj, "BBIM_Fitting")
|
||||||
|
if not fitting_bbim_data:
|
||||||
|
return False
|
||||||
|
|
||||||
|
fitting_bbim_data = fitting_bbim_data["data_dict"]
|
||||||
|
for key in bbim_data:
|
||||||
|
requested_value = bbim_data[key]
|
||||||
|
fitting_value = fitting_bbim_data[key]
|
||||||
|
if isinstance(requested_value, float):
|
||||||
|
compare_precision = None if key == "angle" else precision
|
||||||
|
compare = tool.Cad.is_x(requested_value, fitting_value, compare_precision)
|
||||||
|
elif isinstance(fitting_value, list):
|
||||||
|
compare = tool.Cad.are_vectors_equal(requested_value, Vector(fitting_value), precision)
|
||||||
|
if not compare:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def are_connected_elements_compatible(segments_data, fitting_data):
|
def are_connected_elements_compatible(segments_data, fitting_data):
|
||||||
# prevent arguments mutation, not using deepcopy because of the errors with ifc elements
|
# prevent arguments mutation, not using deepcopy because of the errors with ifc elements
|
||||||
segments_data = [copy(i) for i in segments_data]
|
segments_data = [copy(i) for i in segments_data]
|
||||||
@@ -392,7 +423,7 @@ class MEPGenerator:
|
|||||||
|
|
||||||
# NOTE: I have a feeling that there are cases where order
|
# NOTE: I have a feeling that there are cases where order
|
||||||
# in which we're checking the segments is important
|
# in which we're checking the segments is important
|
||||||
# but I couldn't pin it down exact cases
|
# but I couldn't pin it down to exact cases
|
||||||
for test_segment_data in fitting_data[:]:
|
for test_segment_data in fitting_data[:]:
|
||||||
for base_segment_data in segments_data:
|
for base_segment_data in segments_data:
|
||||||
if not are_segments_compatible(test_segment_data, base_segment_data):
|
if not are_segments_compatible(test_segment_data, base_segment_data):
|
||||||
@@ -451,12 +482,14 @@ class MEPGenerator:
|
|||||||
|
|
||||||
fitting_data.append((element_type, port.PredefinedType, port.SystemType))
|
fitting_data.append((element_type, port.PredefinedType, port.SystemType))
|
||||||
|
|
||||||
# if we skipped the occurrence we still can other occurrences
|
# if we skipped the occurrence we still need to check other occurrences
|
||||||
# otherwise checking 1 occurrence is enough
|
# otherwise checking 1 occurrence is enough
|
||||||
if not skipped_the_occurrence:
|
if not skipped_the_occurrence:
|
||||||
if are_connected_elements_compatible(segments_data, fitting_data):
|
if compatible_with_bbim_data(fitting_type) and are_connected_elements_compatible(
|
||||||
|
segments_data, fitting_data
|
||||||
|
):
|
||||||
return pack_return_data(fitting_type, ports, segments_data)
|
return pack_return_data(fitting_type, ports, segments_data)
|
||||||
return
|
break
|
||||||
|
|
||||||
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"
|
||||||
@@ -596,6 +629,9 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
end_length: bpy.props.FloatProperty(
|
end_length: bpy.props.FloatProperty(
|
||||||
name="End Length", description="Transition end length in SI units", default=0.1, subtype="DISTANCE"
|
name="End Length", description="Transition end length in SI units", default=0.1, subtype="DISTANCE"
|
||||||
)
|
)
|
||||||
|
angle: bpy.props.FloatProperty(
|
||||||
|
name="Transition Angle", description="Transition angle in degrees", default=pi / 6, subtype="ANGLE"
|
||||||
|
)
|
||||||
start_segment_id: bpy.props.IntProperty(name="Start Segment Element ID", default=0)
|
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)
|
end_segment_id: bpy.props.IntProperty(name="End Segment Element ID", default=0)
|
||||||
|
|
||||||
@@ -685,23 +721,18 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
flip_profile_offset = base_transition_dir.dot(start_object_z_basis) < 0
|
flip_profile_offset = base_transition_dir.dot(start_object_z_basis) < 0
|
||||||
|
|
||||||
if tool.Cad.are_edges_collinear(start_axis, end_axis):
|
if tool.Cad.are_edges_collinear(start_axis, end_axis):
|
||||||
profile_offset = None
|
profile_offset = V(0, 0)
|
||||||
else:
|
else:
|
||||||
to_start_object_space = start_object_rotation.inverted()
|
to_start_object_space = start_object_rotation.inverted()
|
||||||
profile_offset = (
|
profile_offset = (
|
||||||
(to_start_object_space @ end_object.location) - (to_start_object_space @ start_object.location)
|
(to_start_object_space @ end_object.location) - (to_start_object_space @ start_object.location)
|
||||||
).xy
|
).xy
|
||||||
if tool.Cad.is_x(profile_offset.length_squared, 0):
|
profile_offset = profile_offset / si_conversion
|
||||||
profile_offset = None
|
if flip_profile_offset:
|
||||||
else:
|
profile_offset *= V(1, -1)
|
||||||
profile_offset = profile_offset / si_conversion
|
|
||||||
if flip_profile_offset:
|
|
||||||
profile_offset *= V(1, -1)
|
|
||||||
|
|
||||||
# world space profile offset
|
# world space profile offset
|
||||||
profile_offset_ws = (
|
profile_offset_ws = start_object_rotation @ (profile_offset * si_conversion).to_3d()
|
||||||
start_object_rotation @ (profile_offset * si_conversion).to_3d() if profile_offset else V(0, 0, 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_segments_length():
|
def get_segments_length():
|
||||||
start_dir = (start_point - first_segment_start).normalized()
|
start_dir = (start_point - first_segment_start).normalized()
|
||||||
@@ -721,6 +752,7 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
end_element,
|
end_element,
|
||||||
self.start_length / si_conversion,
|
self.start_length / si_conversion,
|
||||||
self.end_length / si_conversion,
|
self.end_length / si_conversion,
|
||||||
|
angle=degrees(self.angle),
|
||||||
profile_offset=profile_offset,
|
profile_offset=profile_offset,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -760,9 +792,16 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
DumbProfileJoiner().join_E(start_object, start_segment_extend_point, start_connection)
|
DumbProfileJoiner().join_E(start_object, start_segment_extend_point, start_connection)
|
||||||
DumbProfileJoiner().join_E(end_object, end_segment_extend_point, end_connection)
|
DumbProfileJoiner().join_E(end_object, end_segment_extend_point, end_connection)
|
||||||
|
|
||||||
|
parametric_data = {
|
||||||
|
"start_length": self.start_length / si_conversion,
|
||||||
|
"end_length": self.end_length / si_conversion,
|
||||||
|
"profile_offset": profile_offset,
|
||||||
|
"angle": degrees(self.angle),
|
||||||
|
}
|
||||||
|
|
||||||
# find the compatible fitting type
|
# find the compatible fitting type
|
||||||
fitting_data = MEPGenerator().get_compatible_fitting_type(
|
fitting_data = MEPGenerator().get_compatible_fitting_type(
|
||||||
[start_element, end_element], [start_port, end_port], "TRANSITION"
|
[start_element, end_element], [start_port, end_port], "TRANSITION", bbim_data=parametric_data
|
||||||
)
|
)
|
||||||
transition_type = fitting_data["fitting_type"] if fitting_data else None
|
transition_type = fitting_data["fitting_type"] if fitting_data else None
|
||||||
start_port_match = fitting_data["start_port_match"] if fitting_data else True
|
start_port_match = fitting_data["start_port_match"] if fitting_data else True
|
||||||
@@ -818,7 +857,6 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
start_port, end_port = end_port, start_port
|
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[0], port2=start_port, direction="NOTDEFINED")
|
||||||
tool.Ifc.run("system.connect_port", port1=ports[1], port2=end_port, direction="NOTDEFINED")
|
tool.Ifc.run("system.connect_port", port1=ports[1], port2=end_port, direction="NOTDEFINED")
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -872,7 +910,9 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
# check rotation difference
|
# check rotation difference
|
||||||
def rotation_difference_check():
|
def rotation_difference_check():
|
||||||
end_object_rotation = end_object.matrix_world.to_quaternion()
|
end_object_rotation = end_object.matrix_world.to_quaternion()
|
||||||
rotation_difference = start_object.matrix_world.to_quaternion().rotation_difference(end_object_rotation).to_euler()
|
rotation_difference = (
|
||||||
|
start_object.matrix_world.to_quaternion().rotation_difference(end_object_rotation).to_euler()
|
||||||
|
)
|
||||||
|
|
||||||
def is_multiple_of_pi(value):
|
def is_multiple_of_pi(value):
|
||||||
n = round(value / pi)
|
n = round(value / pi)
|
||||||
@@ -900,8 +940,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
if not types_check():
|
if not types_check():
|
||||||
self.report(
|
self.report(
|
||||||
{"ERROR"},
|
{"ERROR"},
|
||||||
"Segments types do not match "
|
"Segments types do not match " "or one of the segments doesn't have type which is required for a bend.",
|
||||||
"or one of the segments doesn't have type which is required for a bend.",
|
|
||||||
)
|
)
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ class Cad:
|
|||||||
return (x + tolerance) > value > (x - tolerance)
|
return (x + tolerance) > value > (x - tolerance)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def are_vectors_equal(cls, v1: Vector, v2: Vector):
|
def are_vectors_equal(cls, v1: Vector, v2: Vector, tolerance: float = None):
|
||||||
return cls.is_x((v2 - v1).length, 0)
|
return cls.is_x((v2 - v1).length, 0, tolerance)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def intersect_edges(cls, edge1, edge2):
|
def intersect_edges(cls, edge1, edge2):
|
||||||
|
|||||||
@@ -917,7 +917,7 @@ class ShapeBuilder:
|
|||||||
|
|
||||||
# TODO: move MEP to separate shape builder sub module
|
# TODO: move MEP to separate shape builder sub module
|
||||||
def mep_transition_shape(
|
def mep_transition_shape(
|
||||||
self, start_segment, end_segment, start_length, end_length, angle=30.0, profile_offset=None
|
self, start_segment, end_segment, start_length, end_length, angle=30.0, profile_offset=V(0, 0).freeze()
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
returns tuple of Model/Body/MODEL_VIEW IfcRepresentation and transition shape data
|
returns tuple of Model/Body/MODEL_VIEW IfcRepresentation and transition shape data
|
||||||
@@ -979,8 +979,7 @@ class ShapeBuilder:
|
|||||||
|
|
||||||
faces = []
|
faces = []
|
||||||
end_extrusion_offset.z += transition_length
|
end_extrusion_offset.z += transition_length
|
||||||
if profile_offset:
|
end_extrusion_offset.xy += profile_offset
|
||||||
end_extrusion_offset.xy += profile_offset
|
|
||||||
|
|
||||||
if start_profile.is_a("IfcRectangleProfileDef") and end_profile.is_a("IfcRectangleProfileDef"):
|
if start_profile.is_a("IfcRectangleProfileDef") and end_profile.is_a("IfcRectangleProfileDef"):
|
||||||
# no transitions for exactly the same profiles
|
# no transitions for exactly the same profiles
|
||||||
@@ -1115,10 +1114,12 @@ class ShapeBuilder:
|
|||||||
|
|
||||||
body = ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW")
|
body = ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW")
|
||||||
representation = self.get_representation(body, transition_items, "Tesselation")
|
representation = self.get_representation(body, transition_items, "Tesselation")
|
||||||
|
|
||||||
transition_data = {
|
transition_data = {
|
||||||
"start_length": start_length,
|
"start_length": start_length,
|
||||||
"end_length": end_length,
|
"end_length": end_length,
|
||||||
"angle": angle,
|
"angle": angle,
|
||||||
|
"profile_offset": profile_offset,
|
||||||
"transition_length": transition_length,
|
"transition_length": transition_length,
|
||||||
"full_transition_length": start_length + transition_length + end_length,
|
"full_transition_length": start_length + transition_length + end_length,
|
||||||
}
|
}
|
||||||
@@ -1127,7 +1128,7 @@ class ShapeBuilder:
|
|||||||
|
|
||||||
# TODO: move to separate shape_builder method
|
# TODO: move to separate shape_builder method
|
||||||
# so we could check transition length without creating representation
|
# so we could check transition length without creating representation
|
||||||
def mep_transition_length(self, start_half_dim, end_half_dim, angle, profile_offset=None, verbose=True):
|
def mep_transition_length(self, start_half_dim, end_half_dim, angle, profile_offset=V(0, 0).freeze(), verbose=True):
|
||||||
"""get the final transition length for two profiles dimensions, angle and XY offset between them,
|
"""get the final transition length for two profiles dimensions, angle and XY offset between them,
|
||||||
|
|
||||||
the difference from `calculate_transition` - `get_transition_length` is making sure
|
the difference from `calculate_transition` - `get_transition_length` is making sure
|
||||||
@@ -1138,7 +1139,7 @@ class ShapeBuilder:
|
|||||||
# offsets tend to have bunch of float point garbage
|
# offsets tend to have bunch of float point garbage
|
||||||
# that can result in errors when we're calculating value for square root below
|
# that can result in errors when we're calculating value for square root below
|
||||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||||
offset = V(0, 0) if profile_offset is None else round_vector_to_precision(profile_offset, si_conversion)
|
offset = round_vector_to_precision(profile_offset, si_conversion)
|
||||||
diff = start_half_dim.xy - end_half_dim.xy
|
diff = start_half_dim.xy - end_half_dim.xy
|
||||||
diff = Vector([abs(i) for i in diff])
|
diff = Vector([abs(i) for i in diff])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user