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:
Andrej730
2023-09-08 19:23:03 +05:00
parent d4e4717857
commit bf80e4b5e1
3 changed files with 71 additions and 31 deletions
@@ -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"}
+2 -2
View File
@@ -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])