Files
2026-09-08 11:50:32 -07:00

69 lines
2.8 KiB
Python

"""Check an exported stand with Blender's independent triangle BVH.
blender --background --python-exit-code 1 --python script/inspect_stand.py -- stand.3mf report.json
"""
import json
import sys
import zipfile
import xml.etree.ElementTree as ET
from mathutils import Vector
from mathutils.bvhtree import BVHTree
source, output = sys.argv[sys.argv.index("--") + 1:]
with zipfile.ZipFile(source) as package:
root = ET.fromstring(package.read("3D/3dmodel.model"))
ns = {"m": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02"}
meshes = root.findall("m:resources/m:object/m:mesh", ns)
assert len(meshes) == 1, "Stand must contain exactly one mesh"
mesh = meshes[0]
vertices = [Vector(tuple(float(v.attrib[a]) for a in ("x", "y", "z")))
for v in mesh.findall("m:vertices/m:vertex", ns)]
faces = [tuple(int(t.attrib[a]) for a in ("v1", "v2", "v3"))
for t in mesh.findall("m:triangles/m:triangle", ns)]
edges = {}
volume = 0.0
flat_area = 0.0
minimum_area = float("inf")
for i, face in enumerate(faces):
a, b, c = (vertices[j] for j in face)
normal = (b-a).cross(c-a)
area = normal.length / 2
minimum_area = min(minimum_area, area)
volume += a.dot(normal) / 6
if max(abs(p.z) for p in (a, b, c)) < 1e-6:
flat_area += area
for j in range(3):
u, v = face[j], face[(j+1) % 3]
edges.setdefault(tuple(sorted((u, v))), []).append((i, u < v))
assert all(len(uses) == 2 and uses[0][1] != uses[1][1] for uses in edges.values()), "Open edges or inconsistent winding"
adjacency = [[] for _ in faces]
for uses in edges.values():
a, b = uses[0][0], uses[1][0]
adjacency[a].append(b)
adjacency[b].append(a)
seen = {0}
queue = [0]
for face in queue:
for other in adjacency[face]:
if other not in seen:
seen.add(other)
queue.append(other)
assert len(seen) == len(faces), "Disconnected shells"
assert volume > 0 and minimum_area > 1e-10 and flat_area > 0
tree = BVHTree.FromPolygons(vertices, faces, all_triangles=True, epsilon=0.0)
intersections = []
for a, b in tree.overlap(tree):
if a >= b or set(faces[a]).intersection(faces[b]):
continue
intersections.append((a, b))
report = {"vertices": len(vertices), "triangles": len(faces), "connected_components": 1,
"watertight_consistent_winding": True, "signed_volume_mm3": volume,
"table_contact_area_mm2": flat_area, "minimum_triangle_area_mm2": minimum_area,
"nonadjacent_triangle_intersections": len(intersections), "intersection_examples": intersections[:10],
"slicer_validation": "This script checks the mesh only; consult separate slicer results when available."}
with open(output, "w") as stream:
json.dump(report, stream, indent=2)
print(json.dumps(report, indent=2))
assert not intersections, "Nonadjacent triangles intersect"