mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-25 17:57:02 +00:00
Add support for editing curves
This commit is contained in:
@@ -120,217 +120,6 @@ class Helper:
|
|||||||
|
|
||||||
return {"profile": profile, "extrusion": extrusion}
|
return {"profile": profile, "extrusion": extrusion}
|
||||||
|
|
||||||
def auto_detect_profiles(
|
|
||||||
self, obj: bpy.types.Object, mesh: bpy.types.Mesh, position: Matrix | None = None
|
|
||||||
) -> Union[tuple, dict]:
|
|
||||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
|
||||||
|
|
||||||
if position is None:
|
|
||||||
position = Matrix()
|
|
||||||
position_i = position.inverted()
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
tmp = ifcopenshell.file(schema=tool.Ifc.get().schema)
|
|
||||||
|
|
||||||
def is_in_group(v, group_name):
|
|
||||||
for group_index in groups[group_name]:
|
|
||||||
if group_index in v[deform_layer]:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_group_index(v, group_name):
|
|
||||||
for group_index in groups[group_name]:
|
|
||||||
if group_index in v[deform_layer]:
|
|
||||||
return group_index
|
|
||||||
|
|
||||||
# Convert all loops into IFC curves
|
|
||||||
curves = []
|
|
||||||
for loop in loops:
|
|
||||||
|
|
||||||
if len(loop) == 1 and all([is_in_group(v, "IFCCIRCLE") for v in loop[0].verts]):
|
|
||||||
v1, v2 = loop[0].verts
|
|
||||||
mid = v1.co.lerp(v2.co, 0.5)
|
|
||||||
mid = (position_i @ (mid / unit_scale)).to_2d()
|
|
||||||
v1 = (position_i @ (v1.co / unit_scale)).to_2d()
|
|
||||||
radius = (mid - v1).length
|
|
||||||
curves.append(
|
|
||||||
tmp.createIfcCircle(tmp.createIfcAxis2Placement2D(tmp.createIfcCartesianPoint(list(mid))), radius)
|
|
||||||
)
|
|
||||||
else: # For now, assume closed loop
|
|
||||||
loop_verts = []
|
|
||||||
for i, edge in enumerate(loop):
|
|
||||||
if i == 0:
|
|
||||||
if edge.verts[0] in loop[i + 1].verts:
|
|
||||||
loop_verts.append(edge.verts[1])
|
|
||||||
loop_verts.append(edge.verts[0])
|
|
||||||
elif edge.verts[1] in loop[i + 1].verts:
|
|
||||||
loop_verts.append(edge.verts[0])
|
|
||||||
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 tmp.schema != "IFC2X3" and any([is_in_group(v, "IFCARCINDEX") for v in loop_verts]):
|
|
||||||
# We need to specify segments
|
|
||||||
coord_list = [list((position_i @ (v.co / unit_scale)).to_2d()) for v in loop_verts]
|
|
||||||
points = tmp.createIfcCartesianPointList2D(coord_list)
|
|
||||||
i = 0
|
|
||||||
segments = []
|
|
||||||
total_verts = len(loop_verts)
|
|
||||||
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")
|
|
||||||
):
|
|
||||||
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
|
|
||||||
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])
|
|
||||||
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])
|
|
||||||
points = tmp.createIfcCartesianPointList2D(coord_list)
|
|
||||||
curves.append(tmp.createIfcIndexedPolyCurve(points))
|
|
||||||
|
|
||||||
# Sort IFC curves into either closed, or closed with void profile defs
|
|
||||||
profile_defs = []
|
|
||||||
settings = ifcopenshell.geom.settings()
|
|
||||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
|
||||||
|
|
||||||
# First convert to Shapely
|
|
||||||
polygons = {}
|
|
||||||
for curve in curves:
|
|
||||||
geometry = ifcopenshell.geom.create_shape(settings, curve)
|
|
||||||
v = ifcopenshell.util.shape.get_vertices(geometry, is_2d=True)
|
|
||||||
v = np.round(v, 4) # Round to nearest 0.1mm, otherwise things like circles don't polygonise reliably
|
|
||||||
edges = ifcopenshell.util.shape.get_edges(geometry)
|
|
||||||
boundary_lines = [shapely.LineString([v[e[0]], v[e[1]]]) for e in edges]
|
|
||||||
unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines))
|
|
||||||
closed_polygons = shapely.polygonize(unioned_boundaries.geoms)
|
|
||||||
for polygon in closed_polygons.geoms:
|
|
||||||
polygons[curve] = polygon
|
|
||||||
break
|
|
||||||
|
|
||||||
# Check for contains properly (IFC doesn't allow common boundary points)
|
|
||||||
outer_inner = {}
|
|
||||||
inner_outer = {}
|
|
||||||
for curve, polygon in polygons.items():
|
|
||||||
for curve2, polygon2 in polygons.items():
|
|
||||||
if curve == curve2:
|
|
||||||
continue
|
|
||||||
if polygon.contains_properly(polygon2):
|
|
||||||
outer_inner.setdefault(curve, []).append(curve2)
|
|
||||||
inner_outer.setdefault(curve2, []).append(curve)
|
|
||||||
|
|
||||||
# Odd-even rule for nested curves
|
|
||||||
nested_level = {c: len(inner_outer[c]) if c in inner_outer else 0 for c in curves}
|
|
||||||
for curve in sorted(curves, key=lambda c: nested_level[c]):
|
|
||||||
level = nested_level[curve]
|
|
||||||
if level % 2 == 0:
|
|
||||||
if curve in outer_inner:
|
|
||||||
inners = [c for c in outer_inner[curve] if nested_level[c] == level + 1]
|
|
||||||
profile_defs.append(tmp.createIfcArbitraryProfileDefWithVoids("AREA", None, curve, inners))
|
|
||||||
else:
|
|
||||||
profile_defs.append(tmp.createIfcArbitraryClosedProfileDef("AREA", None, curve))
|
|
||||||
|
|
||||||
if len(profile_defs) == 1:
|
|
||||||
profile_def = profile_defs[0]
|
|
||||||
else:
|
|
||||||
profile_def = tmp.createIfcCompositeProfileDef("AREA", None, profile_defs)
|
|
||||||
return {"ifc_file": tmp, "profile_def": profile_def}
|
|
||||||
|
|
||||||
def auto_detect_arbitrary_profile_with_voids(
|
def auto_detect_arbitrary_profile_with_voids(
|
||||||
self, obj: bpy.types.Object, mesh: bpy.types.Mesh
|
self, obj: bpy.types.Object, mesh: bpy.types.Mesh
|
||||||
|
|||||||
@@ -1831,6 +1831,13 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
ProfileDecorator.install(context)
|
ProfileDecorator.install(context)
|
||||||
if not bpy.app.background:
|
if not bpy.app.background:
|
||||||
tool.Blender.set_viewport_tool("bim.cad_tool")
|
tool.Blender.set_viewport_tool("bim.cad_tool")
|
||||||
|
elif tool.Geometry.is_curvelike_item(item):
|
||||||
|
tool.Model.import_curve(item, obj=obj)
|
||||||
|
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
|
||||||
|
self.enable_edit_mode(context)
|
||||||
|
ProfileDecorator.install(context)
|
||||||
|
if not bpy.app.background:
|
||||||
|
tool.Blender.set_viewport_tool("bim.cad_tool")
|
||||||
else:
|
else:
|
||||||
self.report({"INFO"}, f"Editing {item.is_a()} geometry is not supported")
|
self.report({"INFO"}, f"Editing {item.is_a()} geometry is not supported")
|
||||||
|
|
||||||
@@ -2022,6 +2029,50 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
product=element,
|
product=element,
|
||||||
representation=new_footprint,
|
representation=new_footprint,
|
||||||
)
|
)
|
||||||
|
elif tool.Geometry.is_curvelike_item(item):
|
||||||
|
ProfileDecorator.uninstall()
|
||||||
|
new = tool.Model.export_curves(obj)
|
||||||
|
|
||||||
|
if not new:
|
||||||
|
|
||||||
|
def msg(self, context):
|
||||||
|
self.layout.label(text="INVALID PROFILE")
|
||||||
|
|
||||||
|
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||||
|
ProfileDecorator.install(bpy.context)
|
||||||
|
self.enable_edit_mode(bpy.context)
|
||||||
|
return
|
||||||
|
|
||||||
|
additional_curves = []
|
||||||
|
if len(new) > 1:
|
||||||
|
additional_curves = new[1:]
|
||||||
|
new = new[0]
|
||||||
|
|
||||||
|
for inverse in tool.Ifc.get().get_inverse(item):
|
||||||
|
ifcopenshell.util.element.replace_attribute(inverse, item, new)
|
||||||
|
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item)
|
||||||
|
|
||||||
|
obj.data.BIMMeshProperties.ifc_definition_id = new.id()
|
||||||
|
tool.Geometry.import_item(obj)
|
||||||
|
|
||||||
|
props = bpy.context.scene.BIMGeometryProperties
|
||||||
|
for item in additional_curves:
|
||||||
|
representation = tool.Geometry.get_active_representation(props.representation_obj)
|
||||||
|
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||||
|
representation.Items = list(representation.Items) + [item]
|
||||||
|
|
||||||
|
name = f"Item/{item.is_a()}/{item.id()}"
|
||||||
|
mesh = bpy.data.meshes.new(name)
|
||||||
|
new_obj = bpy.data.objects.new(name, mesh)
|
||||||
|
new_obj.data.BIMMeshProperties.ifc_definition_id = item.id()
|
||||||
|
scene = bpy.context.scene
|
||||||
|
scene.collection.objects.link(new_obj)
|
||||||
|
new = props.item_objs.add()
|
||||||
|
new.obj = new_obj
|
||||||
|
new_obj.matrix_world = obj.matrix_world
|
||||||
|
tool.Geometry.import_item(new_obj)
|
||||||
|
|
||||||
|
tool.Geometry.reload_representation(props.representation_obj)
|
||||||
|
|
||||||
def enable_edit_mode(self, context):
|
def enable_edit_mode(self, context):
|
||||||
if tool.Blender.toggle_edit_mode(context) == {"CANCELLED"}:
|
if tool.Blender.toggle_edit_mode(context) == {"CANCELLED"}:
|
||||||
|
|||||||
@@ -551,13 +551,12 @@ class Misc:
|
|||||||
class Model:
|
class Model:
|
||||||
def convert_si_to_unit(cls, value): pass
|
def convert_si_to_unit(cls, value): pass
|
||||||
def convert_unit_to_si(cls, value): pass
|
def convert_unit_to_si(cls, value): pass
|
||||||
def export_curve(cls, position, edge_indices): pass
|
|
||||||
def export_points(cls, position, indices): pass
|
def export_points(cls, position, indices): pass
|
||||||
def export_profile(cls, obj, position=None): pass
|
def export_profile(cls, obj, position=None): pass
|
||||||
def generate_occurrence_name(cls, element_type, ifc_class): pass
|
def generate_occurrence_name(cls, element_type, ifc_class): pass
|
||||||
def get_extrusion(cls, representation): pass
|
def get_extrusion(cls, representation): pass
|
||||||
def import_profile(cls, profile, obj=None, position=None): pass
|
def import_profile(cls, profile, obj=None, position=None): pass
|
||||||
def import_curve(cls, obj, position, curve): pass
|
def import_curve(cls, curve, obj=None, position=None): pass
|
||||||
def import_rectangle(cls, obj, position, profile): pass
|
def import_rectangle(cls, obj, position, profile): pass
|
||||||
def load_openings(cls, openings): pass
|
def load_openings(cls, openings): pass
|
||||||
def clear_scene_openings(cls): pass
|
def clear_scene_openings(cls): pass
|
||||||
|
|||||||
@@ -860,6 +860,15 @@ class Geometry(bonsai.core.tool.Geometry):
|
|||||||
def is_meshlike_item(cls, item: ifcopenshell.entity_instance) -> bool:
|
def is_meshlike_item(cls, item: ifcopenshell.entity_instance) -> bool:
|
||||||
return item.is_a("IfcTessellatedItem") or item.is_a("IfcManifoldSolidBrep")
|
return item.is_a("IfcTessellatedItem") or item.is_a("IfcManifoldSolidBrep")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_curvelike_item(cls, item: ifcopenshell.entity_instance) -> bool:
|
||||||
|
return (
|
||||||
|
item.is_a("IfcPolyline")
|
||||||
|
or item.is_a("IfcCompositeCurve")
|
||||||
|
or item.is_a("IfcIndexedPolyCurve")
|
||||||
|
or item.is_a("IfcCircle")
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_movable(cls, item: ifcopenshell.entity_instance) -> bool:
|
def is_movable(cls, item: ifcopenshell.entity_instance) -> bool:
|
||||||
return item.is_a("IfcSweptAreaSolid") or item.is_a("IfcConic")
|
return item.is_a("IfcSweptAreaSolid") or item.is_a("IfcConic")
|
||||||
@@ -1522,7 +1531,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
|||||||
|
|
||||||
if (is_swept_area := item.is_a("IfcSweptAreaSolid")) or item.is_a("IfcConic"):
|
if (is_swept_area := item.is_a("IfcSweptAreaSolid")) or item.is_a("IfcConic"):
|
||||||
position = item.Position
|
position = item.Position
|
||||||
# Positional is optionaly only for SweptAreaSolid.
|
# Positional is optional only for SweptAreaSolid.
|
||||||
if position or not is_swept_area:
|
if position or not is_swept_area:
|
||||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
position = ifcopenshell.util.placement.get_axis2placement(position)
|
position = ifcopenshell.util.placement.get_axis2placement(position)
|
||||||
|
|||||||
+463
-12
@@ -19,6 +19,7 @@
|
|||||||
import bpy
|
import bpy
|
||||||
import json
|
import json
|
||||||
import bmesh
|
import bmesh
|
||||||
|
import shapely
|
||||||
import collections
|
import collections
|
||||||
import collections.abc
|
import collections.abc
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -86,7 +87,9 @@ class Model(bonsai.core.tool.Model):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_curve(cls, position: Matrix, edge_indices: list[tuple[int, int]]) -> ifcopenshell.entity_instance:
|
def convert_mesh_to_curve(
|
||||||
|
cls, position: Matrix, edge_indices: list[tuple[int, int]]
|
||||||
|
) -> ifcopenshell.entity_instance:
|
||||||
position_i = position.inverted()
|
position_i = position.inverted()
|
||||||
ifc_file = tool.Ifc.get()
|
ifc_file = tool.Ifc.get()
|
||||||
if len(edge_indices) == 2:
|
if len(edge_indices) == 2:
|
||||||
@@ -130,11 +133,24 @@ class Model(bonsai.core.tool.Model):
|
|||||||
if position is None:
|
if position is None:
|
||||||
position = Matrix()
|
position = Matrix()
|
||||||
|
|
||||||
helper = Helper(tool.Ifc.get())
|
result = cls.auto_detect_profiles(obj, obj.data, position)
|
||||||
result = helper.auto_detect_profiles(obj, obj.data, position)
|
|
||||||
if isinstance(result, dict) and result["profile_def"]:
|
if isinstance(result, dict) and result["profile_def"]:
|
||||||
return tool.Ifc.get().add(result["profile_def"])
|
return tool.Ifc.get().add(result["profile_def"])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_curves(
|
||||||
|
cls, obj: bpy.types.Object, position: Optional[Matrix] = None
|
||||||
|
) -> Union[list[ifcopenshell.entity_instance], None]:
|
||||||
|
if position is None:
|
||||||
|
position = Matrix()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
result = cls.auto_detect_curves(obj, obj.data, position)
|
||||||
|
if isinstance(result, dict) and result["curves"]:
|
||||||
|
for curve in result["curves"]:
|
||||||
|
results.append(tool.Ifc.get().add(curve))
|
||||||
|
return results
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_surface(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
|
def export_surface(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
|
||||||
p1, p2, p3 = [v.co.copy() for v in obj.data.vertices[0:3]]
|
p1, p2, p3 = [v.co.copy() for v in obj.data.vertices[0:3]]
|
||||||
@@ -178,10 +194,10 @@ class Model(bonsai.core.tool.Model):
|
|||||||
if tool.Ifc.get().schema != "IFC2X3":
|
if tool.Ifc.get().schema != "IFC2X3":
|
||||||
cls.points = cls.export_points(position, indices["points"])
|
cls.points = cls.export_points(position, indices["points"])
|
||||||
|
|
||||||
surface.OuterBoundary = cls.export_curve(position, indices["profile"])
|
surface.OuterBoundary = cls.convert_mesh_to_curve(position, indices["profile"])
|
||||||
results = []
|
results = []
|
||||||
for inner_curve in indices["inner_curves"]:
|
for inner_curve in indices["inner_curves"]:
|
||||||
results.append(cls.export_curve(position, inner_curve))
|
results.append(cls.convert_mesh_to_curve(position, inner_curve))
|
||||||
surface.InnerBoundaries = results
|
surface.InnerBoundaries = results
|
||||||
|
|
||||||
cls.bm.free()
|
cls.bm.free()
|
||||||
@@ -234,7 +250,7 @@ class Model(bonsai.core.tool.Model):
|
|||||||
)
|
)
|
||||||
cls.edges.append([0, 1])
|
cls.edges.append([0, 1])
|
||||||
else:
|
else:
|
||||||
cls.import_curve(obj, position, axis)
|
cls.convert_curve_to_mesh(obj, position, axis)
|
||||||
|
|
||||||
mesh = bpy.data.meshes.new("Axis")
|
mesh = bpy.data.meshes.new("Axis")
|
||||||
mesh.from_pydata(cls.vertices, cls.edges, [])
|
mesh.from_pydata(cls.vertices, cls.edges, [])
|
||||||
@@ -272,10 +288,10 @@ class Model(bonsai.core.tool.Model):
|
|||||||
profiles = profile.Profiles if profile.is_a("IfcCompositeProfileDef") else [profile]
|
profiles = profile.Profiles if profile.is_a("IfcCompositeProfileDef") else [profile]
|
||||||
for profile in profiles:
|
for profile in profiles:
|
||||||
if profile.is_a("IfcArbitraryClosedProfileDef"):
|
if profile.is_a("IfcArbitraryClosedProfileDef"):
|
||||||
cls.import_curve(obj, position, profile.OuterCurve)
|
cls.convert_curve_to_mesh(obj, position, profile.OuterCurve)
|
||||||
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
|
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
|
||||||
for inner_curve in profile.InnerCurves:
|
for inner_curve in profile.InnerCurves:
|
||||||
cls.import_curve(obj, position, inner_curve)
|
cls.convert_curve_to_mesh(obj, position, inner_curve)
|
||||||
elif profile.is_a() == "IfcRectangleProfileDef":
|
elif profile.is_a() == "IfcRectangleProfileDef":
|
||||||
cls.import_rectangle(obj, position, profile)
|
cls.import_rectangle(obj, position, profile)
|
||||||
|
|
||||||
@@ -301,6 +317,48 @@ class Model(bonsai.core.tool.Model):
|
|||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def import_curve(
|
||||||
|
cls,
|
||||||
|
curve: ifcopenshell.entity_instance,
|
||||||
|
obj: Optional[bpy.types.Object] = None,
|
||||||
|
position: Optional[Matrix] = None,
|
||||||
|
) -> bpy.types.Object:
|
||||||
|
cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
|
|
||||||
|
if position is None:
|
||||||
|
position = Matrix()
|
||||||
|
|
||||||
|
cls.vertices = []
|
||||||
|
cls.edges = []
|
||||||
|
cls.arcs = []
|
||||||
|
cls.circles = []
|
||||||
|
|
||||||
|
if tool.Geometry.is_curvelike_item(curve):
|
||||||
|
cls.convert_curve_to_mesh(obj, position, curve)
|
||||||
|
|
||||||
|
mesh = bpy.data.meshes.new("Curve")
|
||||||
|
mesh.from_pydata(cls.vertices, cls.edges, [])
|
||||||
|
mesh.BIMMeshProperties.subshape_type = "PROFILE"
|
||||||
|
|
||||||
|
if obj is None:
|
||||||
|
obj = bpy.data.objects.new("Curve", mesh)
|
||||||
|
else:
|
||||||
|
old_data = obj.data
|
||||||
|
obj.data = mesh
|
||||||
|
if old_data and not old_data.users:
|
||||||
|
bpy.data.meshes.remove(old_data)
|
||||||
|
|
||||||
|
for arc in cls.arcs:
|
||||||
|
group = obj.vertex_groups.new(name="IFCARCINDEX")
|
||||||
|
group.add(arc, 1, "REPLACE")
|
||||||
|
|
||||||
|
for circle in cls.circles:
|
||||||
|
group = obj.vertex_groups.new(name="IFCCIRCLE")
|
||||||
|
group.add(circle, 1, "REPLACE")
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def import_surface(
|
def import_surface(
|
||||||
cls, surface: ifcopenshell.entity_instance, obj: Optional[bpy.types.Object] = None
|
cls, surface: ifcopenshell.entity_instance, obj: Optional[bpy.types.Object] = None
|
||||||
@@ -316,9 +374,9 @@ class Model(bonsai.core.tool.Model):
|
|||||||
position = Matrix(ifcopenshell.util.placement.get_axis2placement(surface.BasisSurface.Position).tolist())
|
position = Matrix(ifcopenshell.util.placement.get_axis2placement(surface.BasisSurface.Position).tolist())
|
||||||
position.translation *= cls.unit_scale
|
position.translation *= cls.unit_scale
|
||||||
|
|
||||||
cls.import_curve(obj, position, surface.OuterBoundary)
|
cls.convert_curve_to_mesh(obj, position, surface.OuterBoundary)
|
||||||
for inner_boundary in surface.InnerBoundaries:
|
for inner_boundary in surface.InnerBoundaries:
|
||||||
cls.import_curve(obj, position, inner_boundary)
|
cls.convert_curve_to_mesh(obj, position, inner_boundary)
|
||||||
|
|
||||||
mesh = bpy.data.meshes.new("Surface")
|
mesh = bpy.data.meshes.new("Surface")
|
||||||
mesh.from_pydata(cls.vertices, cls.edges, [])
|
mesh.from_pydata(cls.vertices, cls.edges, [])
|
||||||
@@ -340,7 +398,9 @@ class Model(bonsai.core.tool.Model):
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def import_curve(cls, obj: bpy.types.Object, position: Matrix, curve: ifcopenshell.entity_instance) -> None:
|
def convert_curve_to_mesh(
|
||||||
|
cls, obj: bpy.types.Object, position: Matrix, curve: ifcopenshell.entity_instance
|
||||||
|
) -> None:
|
||||||
offset = len(cls.vertices)
|
offset = len(cls.vertices)
|
||||||
|
|
||||||
if curve.is_a("IfcPolyline"):
|
if curve.is_a("IfcPolyline"):
|
||||||
@@ -356,7 +416,7 @@ class Model(bonsai.core.tool.Model):
|
|||||||
elif curve.is_a("IfcCompositeCurve"):
|
elif curve.is_a("IfcCompositeCurve"):
|
||||||
# This is a first pass incomplete implementation only for simple polylines, and misses many details.
|
# This is a first pass incomplete implementation only for simple polylines, and misses many details.
|
||||||
for segment in curve.Segments:
|
for segment in curve.Segments:
|
||||||
cls.import_curve(obj, position, segment.ParentCurve)
|
cls.convert_curve_to_mesh(obj, position, segment.ParentCurve)
|
||||||
elif curve.is_a("IfcIndexedPolyCurve"):
|
elif curve.is_a("IfcIndexedPolyCurve"):
|
||||||
is_arc = False
|
is_arc = False
|
||||||
is_closed = False
|
is_closed = False
|
||||||
@@ -1450,3 +1510,394 @@ class Model(bonsai.core.tool.Model):
|
|||||||
)
|
)
|
||||||
tool.Model.replace_object_ifc_representation(body, obj, representation)
|
tool.Model.replace_object_ifc_representation(body, obj, representation)
|
||||||
tool.Ifc.finish_edit(obj)
|
tool.Ifc.finish_edit(obj)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def auto_detect_profiles(
|
||||||
|
cls, obj: bpy.types.Object, mesh: bpy.types.Mesh, position: Matrix | None = None
|
||||||
|
) -> Union[tuple, dict]:
|
||||||
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
|
|
||||||
|
if position is None:
|
||||||
|
position = Matrix()
|
||||||
|
position_i = position.inverted()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
tmp = ifcopenshell.file(schema=tool.Ifc.get().schema)
|
||||||
|
|
||||||
|
def is_in_group(v, group_name):
|
||||||
|
for group_index in groups[group_name]:
|
||||||
|
if group_index in v[deform_layer]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_group_index(v, group_name):
|
||||||
|
for group_index in groups[group_name]:
|
||||||
|
if group_index in v[deform_layer]:
|
||||||
|
return group_index
|
||||||
|
|
||||||
|
# Convert all loops into IFC curves
|
||||||
|
curves = []
|
||||||
|
for loop in loops:
|
||||||
|
|
||||||
|
if len(loop) == 1 and all([is_in_group(v, "IFCCIRCLE") for v in loop[0].verts]):
|
||||||
|
v1, v2 = loop[0].verts
|
||||||
|
mid = v1.co.lerp(v2.co, 0.5)
|
||||||
|
mid = (position_i @ (mid / unit_scale)).to_2d()
|
||||||
|
v1 = (position_i @ (v1.co / unit_scale)).to_2d()
|
||||||
|
radius = (mid - v1).length
|
||||||
|
curves.append(
|
||||||
|
tmp.createIfcCircle(tmp.createIfcAxis2Placement2D(tmp.createIfcCartesianPoint(list(mid))), radius)
|
||||||
|
)
|
||||||
|
else: # For now, assume closed loop
|
||||||
|
loop_verts = []
|
||||||
|
for i, edge in enumerate(loop):
|
||||||
|
if i == 0:
|
||||||
|
if edge.verts[0] in loop[i + 1].verts:
|
||||||
|
loop_verts.append(edge.verts[1])
|
||||||
|
loop_verts.append(edge.verts[0])
|
||||||
|
elif edge.verts[1] in loop[i + 1].verts:
|
||||||
|
loop_verts.append(edge.verts[0])
|
||||||
|
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 tmp.schema != "IFC2X3" and any([is_in_group(v, "IFCARCINDEX") for v in loop_verts]):
|
||||||
|
# We need to specify segments
|
||||||
|
coord_list = [list((position_i @ (v.co / unit_scale)).to_2d()) for v in loop_verts]
|
||||||
|
points = tmp.createIfcCartesianPointList2D(coord_list)
|
||||||
|
i = 0
|
||||||
|
segments = []
|
||||||
|
total_verts = len(loop_verts)
|
||||||
|
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")
|
||||||
|
):
|
||||||
|
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
|
||||||
|
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])
|
||||||
|
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])
|
||||||
|
points = tmp.createIfcCartesianPointList2D(coord_list)
|
||||||
|
curves.append(tmp.createIfcIndexedPolyCurve(points))
|
||||||
|
|
||||||
|
# Sort IFC curves into either closed, or closed with void profile defs
|
||||||
|
profile_defs = []
|
||||||
|
settings = ifcopenshell.geom.settings()
|
||||||
|
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||||
|
|
||||||
|
# First convert to Shapely
|
||||||
|
polygons = {}
|
||||||
|
for curve in curves:
|
||||||
|
geometry = ifcopenshell.geom.create_shape(settings, curve)
|
||||||
|
v = ifcopenshell.util.shape.get_vertices(geometry, is_2d=True)
|
||||||
|
v = np.round(v, 4) # Round to nearest 0.1mm, otherwise things like circles don't polygonise reliably
|
||||||
|
edges = ifcopenshell.util.shape.get_edges(geometry)
|
||||||
|
boundary_lines = [shapely.LineString([v[e[0]], v[e[1]]]) for e in edges]
|
||||||
|
unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines))
|
||||||
|
closed_polygons = shapely.polygonize(unioned_boundaries.geoms)
|
||||||
|
for polygon in closed_polygons.geoms:
|
||||||
|
polygons[curve] = polygon
|
||||||
|
break
|
||||||
|
|
||||||
|
# Check for contains properly (IFC doesn't allow common boundary points)
|
||||||
|
outer_inner = {}
|
||||||
|
inner_outer = {}
|
||||||
|
for curve, polygon in polygons.items():
|
||||||
|
for curve2, polygon2 in polygons.items():
|
||||||
|
if curve == curve2:
|
||||||
|
continue
|
||||||
|
if polygon.contains_properly(polygon2):
|
||||||
|
outer_inner.setdefault(curve, []).append(curve2)
|
||||||
|
inner_outer.setdefault(curve2, []).append(curve)
|
||||||
|
|
||||||
|
# Odd-even rule for nested curves
|
||||||
|
nested_level = {c: len(inner_outer[c]) if c in inner_outer else 0 for c in curves}
|
||||||
|
for curve in sorted(curves, key=lambda c: nested_level[c]):
|
||||||
|
level = nested_level[curve]
|
||||||
|
if level % 2 == 0:
|
||||||
|
if curve in outer_inner:
|
||||||
|
inners = [c for c in outer_inner[curve] if nested_level[c] == level + 1]
|
||||||
|
profile_defs.append(tmp.createIfcArbitraryProfileDefWithVoids("AREA", None, curve, inners))
|
||||||
|
else:
|
||||||
|
profile_defs.append(tmp.createIfcArbitraryClosedProfileDef("AREA", None, curve))
|
||||||
|
|
||||||
|
if len(profile_defs) == 1:
|
||||||
|
profile_def = profile_defs[0]
|
||||||
|
else:
|
||||||
|
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
|
||||||
|
) -> Union[tuple, dict]:
|
||||||
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
|
|
||||||
|
if position is None:
|
||||||
|
position = Matrix()
|
||||||
|
position_i = position.inverted()
|
||||||
|
|
||||||
|
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: # Forked loop
|
||||||
|
return (False, "FORKED_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)
|
||||||
|
print('autodetected loops', loops)
|
||||||
|
|
||||||
|
tmp = ifcopenshell.file(schema=tool.Ifc.get().schema)
|
||||||
|
|
||||||
|
def is_in_group(v, group_name):
|
||||||
|
for group_index in groups[group_name]:
|
||||||
|
if group_index in v[deform_layer]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_group_index(v, group_name):
|
||||||
|
for group_index in groups[group_name]:
|
||||||
|
if group_index in v[deform_layer]:
|
||||||
|
return group_index
|
||||||
|
|
||||||
|
# Convert all loops into IFC curves
|
||||||
|
curves = []
|
||||||
|
for loop in loops:
|
||||||
|
|
||||||
|
if len(loop) == 1 and all([is_in_group(v, "IFCCIRCLE") for v in loop[0].verts]):
|
||||||
|
v1, v2 = loop[0].verts
|
||||||
|
mid = v1.co.lerp(v2.co, 0.5)
|
||||||
|
mid = (position_i @ (mid / unit_scale)).to_2d()
|
||||||
|
v1 = (position_i @ (v1.co / unit_scale)).to_2d()
|
||||||
|
radius = (mid - v1).length
|
||||||
|
curves.append(
|
||||||
|
tmp.createIfcCircle(tmp.createIfcAxis2Placement2D(tmp.createIfcCartesianPoint(list(mid))), radius)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
loop_verts = []
|
||||||
|
for i, edge in enumerate(loop):
|
||||||
|
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])
|
||||||
|
elif edge.verts[1] in loop[i + 1].verts:
|
||||||
|
loop_verts.append(edge.verts[0])
|
||||||
|
loop_verts.append(edge.verts[1])
|
||||||
|
else:
|
||||||
|
loop_verts.append(edge.other_vert(loop_verts[-1]))
|
||||||
|
|
||||||
|
if is_closed := loop_verts[0] == 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 tmp.schema != "IFC2X3" and any([is_in_group(v, "IFCARCINDEX") for v in loop_verts]):
|
||||||
|
# We need to specify segments
|
||||||
|
coord_list = [list((position_i @ (v.co / unit_scale)).to_2d()) for v in loop_verts]
|
||||||
|
points = tmp.createIfcCartesianPointList2D(coord_list)
|
||||||
|
i = 0
|
||||||
|
segments = []
|
||||||
|
total_verts = len(loop_verts)
|
||||||
|
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")
|
||||||
|
):
|
||||||
|
segments.append(tmp.createIfcArcIndex([i + 1, i + 2, i + 3]))
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
segments.append(tmp.createIfcLineIndex([i + 1, i + 2]))
|
||||||
|
i += 1
|
||||||
|
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
|
||||||
|
]
|
||||||
|
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]
|
||||||
|
if is_closed:
|
||||||
|
coord_list.append(coord_list[0])
|
||||||
|
points = tmp.createIfcCartesianPointList2D(coord_list)
|
||||||
|
curves.append(tmp.createIfcIndexedPolyCurve(points))
|
||||||
|
|
||||||
|
return {"ifc_file": tmp, "curves": curves}
|
||||||
|
|||||||
Reference in New Issue
Block a user