From fb714e8fd808394a99af1eff2ec3082e741c2b3e Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 11 Oct 2024 21:33:10 +1100 Subject: [PATCH] Fix #5482. Reimplement handling of arc detection to allow consecutive arcs. --- .../bonsai/bim/module/geometry/helper.py | 168 -------------- .../bonsai/bim/module/model/decorator.py | 16 +- src/bonsai/bonsai/bim/module/model/slab.py | 3 - src/bonsai/bonsai/tool/blender.py | 10 + src/bonsai/bonsai/tool/model.py | 206 ++++++++---------- 5 files changed, 113 insertions(+), 290 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/geometry/helper.py b/src/bonsai/bonsai/bim/module/geometry/helper.py index eaff04eaf4..5251f842b8 100644 --- a/src/bonsai/bonsai/bim/module/geometry/helper.py +++ b/src/bonsai/bonsai/bim/module/geometry/helper.py @@ -120,174 +120,6 @@ class Helper: return {"profile": profile, "extrusion": extrusion} - - def auto_detect_arbitrary_profile_with_voids( - self, obj: bpy.types.Object, mesh: bpy.types.Mesh - ) -> Union[tuple, dict]: - groups = {"IFCARCINDEX": [], "IFCCIRCLE": []} - for i, group in enumerate(obj.vertex_groups): - if "IFCARCINDEX" in group.name: - groups["IFCARCINDEX"].append(i) - elif "IFCCIRCLE" in group.name: - groups["IFCCIRCLE"].append(i) - - bm = bmesh.new() - bm.from_mesh(mesh) - bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5) - bmesh.ops.delete(bm, geom=bm.faces, context="FACES_ONLY") - - # https://docs.blender.org/api/blender_python_api_2_63_8/bmesh.html#CustomDataAccess - # This is how we access vertex groups via bmesh, apparently, it's not very intuitive - deform_layer = bm.verts.layers.deform.active - - # Sanity check - group_verts = {"IFCARCINDEX": {}, "IFCCIRCLE": {}} - for vert in bm.verts: - total_groups = 0 - is_circle = False - for group_type, group_indices in groups.items(): - if not group_indices: - continue - is_special, group_index = tool.Blender.bmesh_check_vertex_in_groups(vert, deform_layer, group_indices) - if not is_special: - continue - if group_type == "IFCCIRCLE": - is_circle = True - group_verts[group_type].setdefault(group_index, 0) - group_verts[group_type][group_index] += 1 - total_groups += 0 - if total_groups > 1: # A vert can only belong to one group - return (False, "AMBIGUOUS_SPECIAL_VERTEX") - elif is_circle: - pass # Circles are allowed to be unclosed - elif total_groups == 0 and len(vert.link_edges) != 2: # Unclosed loop or forked loop - return (False, "UNCLOSED_LOOP") - - for group_type, group_counts in group_verts.items(): - if group_type == "IFCARCINDEX": - for group_count in group_counts.values(): - if group_count != 3: # Each arc needs 3 verts - return (False, "3POINT_ARC") - elif group_type == "IFCCIRCLE": - for group_count in group_counts.values(): - if group_count != 2: # Each circle needs 2 verts - return (False, "CIRCLE") - - loop_edges = set(bm.edges) - - # Create loops from edges - loops = [] - while loop_edges: - edge = loop_edges.pop() - loop = [edge] - has_found_connected_edge = True - while has_found_connected_edge: - has_found_connected_edge = False - for edge in loop_edges.copy(): - edge_verts = set(edge.verts) - if edge_verts & set(loop[0].verts): - loop.insert(0, edge) - loop_edges.remove(edge) - has_found_connected_edge = True - elif edge_verts & set(loop[-1].verts): - loop.append(edge) - loop_edges.remove(edge) - has_found_connected_edge = True - loops.append(loop) - - # Determine outer loop - max_area = 0 - outer_loop = None - inner_loops = [] - - for loop in loops: - loop_vertices = [] - total_edges = len(loop) - - if total_edges == 1: - loop_vertices = [loop[0].verts[0], loop[0].verts[1]] - else: - for i, edge in enumerate(loop): - if i + 1 == total_edges and edge.verts[0] in loop[i - 1].verts: - loop_vertices.append(edge.verts[0]) - elif i + 1 == total_edges and edge.verts[1] in loop[i - 1].verts: - loop_vertices.append(edge.verts[1]) - elif edge.verts[0] in loop[i + 1].verts: - loop_vertices.append(edge.verts[1]) - elif edge.verts[1] in loop[i + 1].verts: - loop_vertices.append(edge.verts[0]) - - if len(loop_vertices) == 2: - # Unofficial convention right now that a loop of 2 verts always is a circle - radius = (loop_vertices[0].co - loop_vertices[1].co).length / 2 - face_area = pi * pow(radius, 2) - else: - loop_bm = bmesh.new() - for vert in loop_vertices: - loop_bm.verts.new(vert.co) - face = loop_bm.faces.new(loop_bm.verts) - face_area = face.calc_area() - loop_bm.free() - - loop_vertex_indices = [] - active_arc_id = None - arc_stack = [] - last_index = len(loop_vertices) - 1 - - # It is possible to loop through loop_vertices halfway through an arc. - # If that is the case, we store it in incomplete_arc - incomplete_arc = [] - for i, v in enumerate(loop_vertices): - if len(arc_stack) == 3: - loop_vertex_indices.append(arc_stack) - loop_vertex_indices.append((arc_stack[-1], v.index)) - arc_stack = [] - active_arc_id = None - - is_arc = False - for group_index in groups["IFCARCINDEX"]: - if group_index in v[deform_layer]: - if active_arc_id is not None and active_arc_id != group_index: - incomplete_arc = arc_stack - arc_stack = [] - active_arc_id = group_index - is_arc = True - break - - if i == last_index and not is_arc: - continue - - if is_arc: - arc_stack.append(v.index) - else: - if active_arc_id is not None: - incomplete_arc = arc_stack - active_arc_id = None - arc_stack = [] - loop_vertex_indices.append((v.index, loop_vertices[i + 1].index)) - - if active_arc_id is not None: - arc_stack.extend(incomplete_arc) - loop_vertex_indices.append(arc_stack) - - loop_vertex_indices.append((loop_vertex_indices[-1][-1], loop_vertex_indices[0][0])) - - if face_area > max_area: - max_area = face_area - outer_loop = loop_vertex_indices - inner_loops.append(loop_vertex_indices) - - inner_loops.remove(outer_loop) - - # Copy vectors to prevent random data mangling after bmesh is freed. - points = [v.co.copy() for v in bm.verts] - - bm.to_mesh(mesh) - mesh.update() - bm.free() - - return {"points": points, "profile": outer_loop, "inner_curves": inner_loops} - # An arbitrary closed profile with voids is similar to one without voids. # We start the same way with any ngon (no tri), but instead of being the entire # profile, it is only one of the possible faces that make up the end of our diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index e3723fdf96..74dd1c9080 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -144,15 +144,13 @@ class ProfileDecorator: # deform_layer is None if there are no verts assigned to vertex groups # even if there are vertex groups in the obj.vertex_groups if deform_layer: - is_arc, group_index = tool.Blender.bmesh_check_vertex_in_groups(vertex, deform_layer, arc_groups) - if is_arc: - arcs.setdefault(group_index, []).append(vertex) - special_vertex_indices[vertex.index] = group_index - - is_circle, group_index = tool.Blender.bmesh_check_vertex_in_groups(vertex, deform_layer, circle_groups) - if is_circle: - circles.setdefault(group_index, []).append(vertex) - special_vertex_indices[vertex.index] = group_index + for group_index in tool.Blender.bmesh_get_vertex_groups(vertex, deform_layer): + if is_arc := group_index in arc_groups: + arcs.setdefault(group_index, []).append(vertex) + special_vertex_indices[vertex.index] = group_index + if is_circle := group_index in circle_groups: + circles.setdefault(group_index, []).append(vertex) + special_vertex_indices[vertex.index] = group_index if vertex.select: selected_vertices.append(co) diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index 56c4c7aa65..7a6ac6e96a 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -696,9 +696,6 @@ class SetArcIndex(bpy.types.Operator): return self.cancel_message("Select 3 vertices.") bpy.ops.object.mode_set(mode="OBJECT") - in_group = any([bool(obj.data.vertices[v].groups) for v in selected_vertices]) - for group in obj.vertex_groups: - group.remove(selected_vertices) group = obj.vertex_groups.new(name="IFCARCINDEX") group.add(selected_vertices, 1, "REPLACE") bpy.ops.object.mode_set(mode="EDIT") diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index f0e05f7034..77e35620f4 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -712,6 +712,16 @@ class Blender(bonsai.core.tool.Blender): return True, group_index return False, None + @classmethod + def bmesh_get_vertex_groups(cls, vertex: bmesh.types.BMVert, deform_layer: bmesh.types.BMLayerItem) -> list[int]: + results = [] + for group_index in vertex[deform_layer].keys(): + # Ignore vertex groups assignments produced by edge subdivision near arcs + # They usually have weight = 0.5 + if vertex[deform_layer][group_index] == 1.0: + results.append(group_index) + return results + @classmethod def toggle_edit_mode(cls, context: bpy.types.Context) -> set[str]: ao = context.active_object diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 59c77e42bd..74f3418889 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -128,7 +128,7 @@ class Model(bonsai.core.tool.Model): @classmethod def export_profile( cls, obj: bpy.types.Object, position: Optional[Matrix] = None - ) -> Union[ifcopenshell.entity_instance, None]: + ) -> ifcopenshell.entity_instance | None: """Returns `None` in case if profile was invalid.""" if position is None: position = Matrix() @@ -140,7 +140,7 @@ class Model(bonsai.core.tool.Model): @classmethod def export_curves( cls, obj: bpy.types.Object, position: Optional[Matrix] = None - ) -> Union[list[ifcopenshell.entity_instance], None]: + ) -> list[ifcopenshell.entity_instance] | None: if position is None: position = Matrix() @@ -171,12 +171,15 @@ class Model(bonsai.core.tool.Model): cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - helper = Helper(tool.Ifc.get()) - indices = helper.auto_detect_arbitrary_profile_with_voids(obj, obj.data) + result = cls.auto_detect_profiles(obj, obj.data, position) - if isinstance(indices, tuple) and indices[0] is False: # Ugly + if not isinstance(result, dict): # Ugly return + profile_def = result["profile_def"] + if profile_def.is_a("IfcCompositeProfileDef"): + profile_def = profile_def.Profiles[0] + cls.bm = bmesh.new() cls.bm.from_mesh(obj.data) cls.bm.verts.ensure_lookup_table() @@ -191,14 +194,10 @@ class Model(bonsai.core.tool.Model): ) ) - if tool.Ifc.get().schema != "IFC2X3": - cls.points = cls.export_points(position, indices["points"]) - - surface.OuterBoundary = cls.convert_mesh_to_curve(position, indices["profile"]) - results = [] - for inner_curve in indices["inner_curves"]: - results.append(cls.convert_mesh_to_curve(position, inner_curve)) + surface.OuterBoundary = tool.Ifc.get().add(profile_def.OuterCurve) surface.InnerBoundaries = results + if profile_def.is_a("IfcArbitraryProfileDefWithVoids"): + surface.InnerBoundaries = [tool.Ifc.get().add(c) for c in profile_def.InnerCurves] cls.bm.free() return surface @@ -418,42 +417,26 @@ class Model(bonsai.core.tool.Model): for segment in curve.Segments: cls.convert_curve_to_mesh(obj, position, segment.ParentCurve) elif curve.is_a("IfcIndexedPolyCurve"): - is_arc = False - is_closed = False + for local_point in curve.Points.CoordList: + global_point = position @ Vector(cls.convert_unit_to_si(local_point)).to_3d() + cls.vertices.append(global_point) if curve.Segments: for segment in curve.Segments: if segment.is_a("IfcArcIndex"): - is_arc = True - local_point = cls.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1]) - global_point = position @ Vector(local_point).to_3d() - cls.vertices.append(global_point) - local_point = cls.convert_unit_to_si(curve.Points.CoordList[segment[0][1] - 1]) - global_point = position @ Vector(local_point).to_3d() - cls.vertices.append(global_point) - cls.arcs.append([len(cls.vertices) - 2, len(cls.vertices) - 1]) + cls.arcs.append([i - 1 + offset for i in segment[0]]) + cls.edges.append([i - 1 + offset for i in segment[0][:2]]) + cls.edges.append([i - 1 + offset for i in segment[0][1:]]) else: - for segment_index in segment[0][0:-1]: - local_point = cls.convert_unit_to_si(curve.Points.CoordList[segment_index - 1]) - global_point = position @ Vector(local_point).to_3d() - cls.vertices.append(global_point) - if is_arc: - cls.arcs[-1].append(len(cls.vertices) - 1) - is_arc = False - - if curve.Segments[0][0][0] == curve.Segments[-1][0][-1]: - is_closed = True + segment = [i - 1 + offset for i in segment[0]] + cls.edges.extend(zip(segment, segment[1:])) else: - for local_point in curve.Points.CoordList: - global_point = position @ Vector(cls.convert_unit_to_si(local_point)).to_3d() - cls.vertices.append(global_point) - + is_closed = False if cls.vertices[offset] == cls.vertices[-1]: is_closed = True del cls.vertices[-1] - - cls.edges.extend([(i, i + 1) for i in range(offset, len(cls.vertices) - 1)]) - if is_closed: - cls.edges.append([len(cls.vertices) - 1, offset]) # Close the loop + cls.edges.extend([(i, i + 1) for i in range(offset, len(cls.vertices) - 1)]) + if is_closed: + cls.edges.append([len(cls.vertices) - 1, offset]) # Close the loop elif curve.is_a("IfcCircle"): circle_position = Matrix(ifcopenshell.util.placement.get_axis2placement(curve.Position).tolist()) circle_position.translation *= cls.unit_scale @@ -1538,26 +1521,20 @@ class Model(bonsai.core.tool.Model): # Sanity check group_verts = {"IFCARCINDEX": {}, "IFCCIRCLE": {}} - for vert in bm.verts: - total_groups = 0 - is_circle = False - for group_type, group_indices in groups.items(): - if not group_indices: - continue - is_special, group_index = tool.Blender.bmesh_check_vertex_in_groups(vert, deform_layer, group_indices) - if not is_special: - continue - if group_type == "IFCCIRCLE": - is_circle = True - group_verts[group_type].setdefault(group_index, 0) - group_verts[group_type][group_index] += 1 - total_groups += 0 - if total_groups > 1: # A vert can only belong to one group - return (False, "AMBIGUOUS_SPECIAL_VERTEX") - elif is_circle: - pass # Circles are allowed to be unclosed - elif total_groups == 0 and len(vert.link_edges) != 2: # Unclosed loop or forked loop - return (False, "UNCLOSED_LOOP") + if deform_layer: + for vert in bm.verts: + vert_group_indices = tool.Blender.bmesh_get_vertex_groups(vert, deform_layer) + is_circle = False + for group_index in vert_group_indices: + group_type = "IFCARCINDEX" if group_index in groups["IFCARCINDEX"] else "IFCCIRCLE" + group_verts[group_type].setdefault(group_index, 0) + group_verts[group_type][group_index] += 1 + if group_type == "IFCCIRCLE": + is_circle = True + if is_circle: + pass # Circles are allowed to be unclosed + elif len(vert.link_edges) != 2: # Unclosed loop or forked loop + return (False, "UNCLOSED_LOOP") for group_type, group_counts in group_verts.items(): if group_type == "IFCARCINDEX": @@ -1617,10 +1594,13 @@ class Model(bonsai.core.tool.Model): curves.append( tmp.createIfcCircle(tmp.createIfcAxis2Placement2D(tmp.createIfcCartesianPoint(list(mid))), radius) ) - else: # For now, assume closed loop + else: loop_verts = [] for i, edge in enumerate(loop): - if i == 0: + if i == 0 and len(loop) == 1: + loop_verts.append(edge.verts[0]) + loop_verts.append(edge.verts[1]) + elif i == 0: if edge.verts[0] in loop[i + 1].verts: loop_verts.append(edge.verts[1]) loop_verts.append(edge.verts[0]) @@ -1629,15 +1609,23 @@ class Model(bonsai.core.tool.Model): loop_verts.append(edge.verts[1]) else: loop_verts.append(edge.other_vert(loop_verts[-1])) - loop_verts.pop() - # Handle loop_verts possibly starting halfway through an arc - if (group_index := get_group_index(loop_verts[0], "IFCARCINDEX")) is not None: - if get_group_index(loop_verts[1], "IFCARCINDEX") != group_index: - loop_verts.insert(0, loop_verts.pop()) - loop_verts.insert(0, loop_verts.pop()) - elif get_group_index(loop_verts[2], "IFCARCINDEX") != group_index: - loop_verts.insert(0, loop_verts.pop()) + if is_closed := loop_verts[0] == loop_verts[-1]: + loop_verts.pop() + + # Handle loop_verts possibly starting halfway through an arc + if deform_layer: + if gi := tool.Blender.bmesh_get_vertex_groups(loop_verts[0], deform_layer): + if not (gi2 := tool.Blender.bmesh_get_vertex_groups(loop_verts[1], deform_layer)): + loop_verts.insert(0, loop_verts.pop()) + loop_verts.insert(0, loop_verts.pop()) + elif not (set(gi) & set(gi2)): + loop_verts.insert(0, loop_verts.pop()) + loop_verts.insert(0, loop_verts.pop()) + elif not (gi2 := tool.Blender.bmesh_get_vertex_groups(loop_verts[2], deform_layer)): + loop_verts.insert(0, loop_verts.pop()) + elif not (set(gi) & set(gi2)): + loop_verts.insert(0, loop_verts.pop()) if tmp.schema != "IFC2X3" and any([is_in_group(v, "IFCARCINDEX") for v in loop_verts]): # We need to specify segments @@ -1649,30 +1637,34 @@ class Model(bonsai.core.tool.Model): while i < total_verts: v = loop_verts[i] if ( - i + 1 != total_verts - and is_in_group(v, "IFCARCINDEX") - and is_in_group(loop_verts[i + 1], "IFCARCINDEX") + (i + 1 != total_verts) + and (gi := tool.Blender.bmesh_get_vertex_groups(v, deform_layer)) + and (gi2 := tool.Blender.bmesh_get_vertex_groups(loop_verts[i + 1], deform_layer)) + and (set(gi) & set(gi2)) ): segments.append(tmp.createIfcArcIndex([i + 1, i + 2, i + 3])) i += 2 else: segments.append(tmp.createIfcLineIndex([i + 1, i + 2])) i += 1 - # Close the loop - last_segment_indices = list(segments[-1][0]) - last_segment_indices[-1] = 1 - segments[-1][0] = last_segment_indices + if is_closed: + # Close the loop + last_segment_indices = list(segments[-1][0]) + last_segment_indices[-1] = 1 + segments[-1][0] = last_segment_indices curves.append(tmp.createIfcIndexedPolyCurve(points, segments)) elif tmp.schema == "IFC2X3": points = [ tmp.createIfcCartesianPoint(list((position_i @ (v.co / unit_scale)).to_2d())) for v in loop_verts ] - points.append(points[0]) + if is_closed: + points.append(points[0]) curves.append(tmp.createIfcPolyline(points)) else: # Pure straight polyline, no segments required coord_list = [list((position_i @ (v.co / unit_scale)).to_2d()) for v in loop_verts] - coord_list.append(coord_list[0]) + if is_closed: + coord_list.append(coord_list[0]) points = tmp.createIfcCartesianPointList2D(coord_list) curves.append(tmp.createIfcIndexedPolyCurve(points)) @@ -1723,7 +1715,6 @@ class Model(bonsai.core.tool.Model): profile_def = tmp.createIfcCompositeProfileDef("AREA", None, profile_defs) return {"ifc_file": tmp, "profile_def": profile_def} - @classmethod def auto_detect_curves( cls, obj: bpy.types.Object, mesh: bpy.types.Mesh, position: Matrix | None = None @@ -1752,26 +1743,15 @@ class Model(bonsai.core.tool.Model): # Sanity check group_verts = {"IFCARCINDEX": {}, "IFCCIRCLE": {}} - for vert in bm.verts: - total_groups = 0 - is_circle = False - for group_type, group_indices in groups.items(): - if not group_indices: - continue - is_special, group_index = tool.Blender.bmesh_check_vertex_in_groups(vert, deform_layer, group_indices) - if not is_special: - continue - if group_type == "IFCCIRCLE": - is_circle = True - group_verts[group_type].setdefault(group_index, 0) - group_verts[group_type][group_index] += 1 - total_groups += 0 - if total_groups > 1: # A vert can only belong to one group - return (False, "AMBIGUOUS_SPECIAL_VERTEX") - elif is_circle: - pass # Circles are allowed to be unclosed - elif total_groups == 0 and len(vert.link_edges) > 2: # Forked loop - return (False, "FORKED_LOOP") + if deform_layer: + for vert in bm.verts: + vert_group_indices = tool.Blender.bmesh_get_vertex_groups(vert, deform_layer) + for group_index in vert_group_indices: + group_type = "IFCARCINDEX" if group_index in groups["IFCARCINDEX"] else "IFCCIRCLE" + group_verts[group_type].setdefault(group_index, 0) + group_verts[group_type][group_index] += 1 + if len(vert.link_edges) > 2: # Forked loop + return (False, "FORKED_LOOP") for group_type, group_counts in group_verts.items(): if group_type == "IFCARCINDEX": @@ -1804,7 +1784,6 @@ class Model(bonsai.core.tool.Model): loop_edges.remove(edge) has_found_connected_edge = True loops.append(loop) - print('autodetected loops', loops) tmp = ifcopenshell.file(schema=tool.Ifc.get().schema) @@ -1852,12 +1831,18 @@ class Model(bonsai.core.tool.Model): loop_verts.pop() # Handle loop_verts possibly starting halfway through an arc - if (group_index := get_group_index(loop_verts[0], "IFCARCINDEX")) is not None: - if get_group_index(loop_verts[1], "IFCARCINDEX") != group_index: - loop_verts.insert(0, loop_verts.pop()) - loop_verts.insert(0, loop_verts.pop()) - elif get_group_index(loop_verts[2], "IFCARCINDEX") != group_index: - loop_verts.insert(0, loop_verts.pop()) + if deform_layer: + if gi := tool.Blender.bmesh_get_vertex_groups(loop_verts[0], deform_layer): + if not (gi2 := tool.Blender.bmesh_get_vertex_groups(loop_verts[1], deform_layer)): + loop_verts.insert(0, loop_verts.pop()) + loop_verts.insert(0, loop_verts.pop()) + elif not (set(gi) & set(gi2)): + loop_verts.insert(0, loop_verts.pop()) + loop_verts.insert(0, loop_verts.pop()) + elif not (gi2 := tool.Blender.bmesh_get_vertex_groups(loop_verts[2], deform_layer)): + loop_verts.insert(0, loop_verts.pop()) + elif not (set(gi) & set(gi2)): + loop_verts.insert(0, loop_verts.pop()) if tmp.schema != "IFC2X3" and any([is_in_group(v, "IFCARCINDEX") for v in loop_verts]): # We need to specify segments @@ -1869,9 +1854,10 @@ class Model(bonsai.core.tool.Model): while i < total_verts: v = loop_verts[i] if ( - i + 1 != total_verts - and is_in_group(v, "IFCARCINDEX") - and is_in_group(loop_verts[i + 1], "IFCARCINDEX") + (i + 1 != total_verts) + and (gi := tool.Blender.bmesh_get_vertex_groups(v, deform_layer)) + and (gi2 := tool.Blender.bmesh_get_vertex_groups(loop_verts[i + 1], deform_layer)) + and (set(gi) & set(gi2)) ): segments.append(tmp.createIfcArcIndex([i + 1, i + 2, i + 3])) i += 2