mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 09:48:32 +00:00
This commit is contained in:
@@ -18,9 +18,11 @@
|
||||
|
||||
import bpy
|
||||
import bmesh
|
||||
import shapely
|
||||
import mathutils
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.shape
|
||||
import bonsai.tool as tool
|
||||
from math import pi, pow
|
||||
from mathutils import Vector, Matrix, geometry
|
||||
@@ -117,6 +119,217 @@ class Helper:
|
||||
|
||||
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).to_2d()
|
||||
v1 = (position_i @ v1.co).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)
|
||||
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(
|
||||
self, obj: bpy.types.Object, mesh: bpy.types.Mesh
|
||||
) -> Union[tuple, dict]:
|
||||
|
||||
@@ -130,36 +130,10 @@ class Model(bonsai.core.tool.Model):
|
||||
if position is None:
|
||||
position = Matrix()
|
||||
|
||||
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)
|
||||
|
||||
if isinstance(indices, tuple) and indices[0] is False: # Ugly
|
||||
return
|
||||
|
||||
cls.bm = bmesh.new()
|
||||
cls.bm.from_mesh(obj.data)
|
||||
cls.bm.verts.ensure_lookup_table()
|
||||
cls.bm.edges.ensure_lookup_table()
|
||||
|
||||
if indices["inner_curves"]:
|
||||
profile = tool.Ifc.get().createIfcArbitraryProfileDefWithVoids("AREA")
|
||||
else:
|
||||
profile = tool.Ifc.get().createIfcArbitraryClosedProfileDef("AREA")
|
||||
|
||||
if tool.Ifc.get().schema != "IFC2X3":
|
||||
cls.points = cls.export_points(position, indices["points"])
|
||||
|
||||
profile.OuterCurve = cls.export_curve(position, indices["profile"])
|
||||
if indices["inner_curves"]:
|
||||
results = []
|
||||
for inner_curve in indices["inner_curves"]:
|
||||
results.append(cls.export_curve(position, inner_curve))
|
||||
profile.InnerCurves = results
|
||||
|
||||
cls.bm.free()
|
||||
return profile
|
||||
result = helper.auto_detect_profiles(obj, obj.data, position)
|
||||
if result["profile_def"]:
|
||||
return tool.Ifc.get().add(result["profile_def"])
|
||||
|
||||
@classmethod
|
||||
def export_surface(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
|
||||
@@ -290,13 +264,15 @@ class Model(bonsai.core.tool.Model):
|
||||
cls.arcs = []
|
||||
cls.circles = []
|
||||
|
||||
if profile.is_a("IfcArbitraryClosedProfileDef"):
|
||||
cls.import_curve(obj, position, profile.OuterCurve)
|
||||
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
|
||||
for inner_curve in profile.InnerCurves:
|
||||
cls.import_curve(obj, position, inner_curve)
|
||||
elif profile.is_a() == "IfcRectangleProfileDef":
|
||||
cls.import_rectangle(obj, position, profile)
|
||||
profiles = profile.Profiles if profile.is_a("IfcCompositeProfileDef") else [profile]
|
||||
for profile in profiles:
|
||||
if profile.is_a("IfcArbitraryClosedProfileDef"):
|
||||
cls.import_curve(obj, position, profile.OuterCurve)
|
||||
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
|
||||
for inner_curve in profile.InnerCurves:
|
||||
cls.import_curve(obj, position, inner_curve)
|
||||
elif profile.is_a() == "IfcRectangleProfileDef":
|
||||
cls.import_rectangle(obj, position, profile)
|
||||
|
||||
mesh = bpy.data.meshes.new("Profile")
|
||||
mesh.from_pydata(cls.vertices, cls.edges, [])
|
||||
|
||||
Reference in New Issue
Block a user