mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
Return nan for non-manifold volume and skip it in QTO (#6125)
util.shape.get_volume sums signed tetrahedra (divergence theorem), which is only valid for a closed watertight mesh. For a non-manifold or open mesh (e.g. the IfcPolygonalFaceSet slabs in the report, 19 of 152 edges not shared by exactly two triangles) it returned a wild value: 17.71 m3 against a 1.82 m3 bounding box, a 9.7x overestimate. get_volume now runs an O(faces) edge-parity check and returns nan when the mesh is not a closed manifold, instead of a meaningless number. The ifc5d QTO consumer skips a nan result rather than writing it into the IFC, so a non-manifold element simply gets no volume quantity. Closed manifold volumes are unchanged (a 2x3x0.5 box still reports exactly 3.0). Verified: the three reported slabs go from 17.71 to nan (no NetVolume written), while a manifold box still writes its correct NetVolume. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -445,6 +445,11 @@ class IfcOpenShell(QtoCalculator):
|
||||
value = formula_functions[formula](geometry)
|
||||
assert isinstance(value, (float, int))
|
||||
value = cls.unit_converter.convert(value, IfcOpenShell.raw_functions[formula].measure)
|
||||
# get_volume returns nan for a non-manifold mesh (its volume is
|
||||
# undefined, see #6125); skip such quantities rather than writing
|
||||
# nan into the IFC. The self-inequality test avoids importing math.
|
||||
if isinstance(value, float) and value != value:
|
||||
continue
|
||||
results[element][name][quantity] = value
|
||||
if not iterator.next():
|
||||
break
|
||||
|
||||
@@ -71,10 +71,14 @@ def is_x(value: float, x: float, tolerance: Optional[float] = None) -> bool:
|
||||
def get_volume(geometry: W.Triangulation) -> float:
|
||||
"""Calculates the total internal volume of a geometry
|
||||
|
||||
Volumes of non-manifold geometry will be unpredictable.
|
||||
The volume is derived from the divergence theorem (summing signed
|
||||
tetrahedra), which is only meaningful for a closed manifold (watertight)
|
||||
mesh. For non-manifold or open geometry that value is undefined and can be
|
||||
wildly over- or under-estimated, so ``float("nan")`` is returned instead of
|
||||
a bogus number. See https://github.com/IfcOpenShell/IfcOpenShell/issues/6125.
|
||||
|
||||
:param geometry: Geometry output calculated by IfcOpenShell
|
||||
:return: The volume in m3
|
||||
:return: The volume in m3, or ``nan`` if the mesh is not a closed manifold
|
||||
"""
|
||||
|
||||
# https://stackoverflow.com/questions/1406029/how-to-calculate-the-volume-of-a-3d-mesh-object-the-surface-of-which-is-made-up
|
||||
@@ -90,6 +94,19 @@ def get_volume(geometry: W.Triangulation) -> float:
|
||||
# Can't optimize it using buffers - performance seems to get only worse.
|
||||
verts = geometry.verts
|
||||
faces = geometry.faces
|
||||
|
||||
# A watertight (closed manifold) mesh shares every edge between exactly two
|
||||
# triangles. If that does not hold the signed-tetrahedra sum below is
|
||||
# meaningless, so bail out with nan rather than returning a wild value.
|
||||
edge_face_count: dict[tuple[int, int], int] = {}
|
||||
for i in range(0, len(faces), 3):
|
||||
tri = (faces[i], faces[i + 1], faces[i + 2])
|
||||
for a, b in ((tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])):
|
||||
edge = (a, b) if a < b else (b, a)
|
||||
edge_face_count[edge] = edge_face_count.get(edge, 0) + 1
|
||||
if any(count != 2 for count in edge_face_count.values()):
|
||||
return float("nan")
|
||||
|
||||
grouped_verts = [[verts[i], verts[i + 1], verts[i + 2]] for i in range(0, len(verts), 3)]
|
||||
volumes = [
|
||||
signed_triangle_volume(grouped_verts[faces[i]], grouped_verts[faces[i + 1]], grouped_verts[faces[i + 2]])
|
||||
|
||||
Reference in New Issue
Block a user