mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-19 06:39:13 +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)
|
value = formula_functions[formula](geometry)
|
||||||
assert isinstance(value, (float, int))
|
assert isinstance(value, (float, int))
|
||||||
value = cls.unit_converter.convert(value, IfcOpenShell.raw_functions[formula].measure)
|
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
|
results[element][name][quantity] = value
|
||||||
if not iterator.next():
|
if not iterator.next():
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -71,10 +71,14 @@ def is_x(value: float, x: float, tolerance: Optional[float] = None) -> bool:
|
|||||||
def get_volume(geometry: W.Triangulation) -> float:
|
def get_volume(geometry: W.Triangulation) -> float:
|
||||||
"""Calculates the total internal volume of a geometry
|
"""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
|
: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
|
# 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.
|
# Can't optimize it using buffers - performance seems to get only worse.
|
||||||
verts = geometry.verts
|
verts = geometry.verts
|
||||||
faces = geometry.faces
|
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)]
|
grouped_verts = [[verts[i], verts[i + 1], verts[i + 2]] for i in range(0, len(verts), 3)]
|
||||||
volumes = [
|
volumes = [
|
||||||
signed_triangle_volume(grouped_verts[faces[i]], grouped_verts[faces[i + 1]], grouped_verts[faces[i + 2]])
|
signed_triangle_volume(grouped_verts[faces[i]], grouped_verts[faces[i + 1]], grouped_verts[faces[i + 2]])
|
||||||
|
|||||||
Reference in New Issue
Block a user