nx_meshcheck.nx source
↩ module page · 382 lines · 18066 B
1// nx_meshcheck.nx -- MESH INTEGRITY, the check nothing in this estate performed.
2//
3// WHAT THIS IS AND WHY IT IS MISSING-SHAPED. The estate GENERATES meshes -- surface nets from SDFs,
4// layered bodies, skulls, faces -- and measures them for SHAPE AGREEMENT against a reference
5// (nx_mmdev_lib md_measure, exact per-vertex deviation) and for anatomical plausibility. Nothing
6// asks the prior question: IS THIS MESH STRUCTURALLY WELL-FORMED AT ALL. A mesh can sit perfectly
7// on its reference and still have inverted winding on a third of its faces, an edge shared by three
8// triangles, or four hundred vertices no triangle references -- and every one of those shows up to
9// the eye as junk while every ruler we own reports agreement.
10//
11// THE ONE PRIOR CHECK, AND WHY IT IS NOT ENOUGH. nx_faceanat_mesh_gate carries a WATERTIGHT tooth
12// using the Euler characteristic: for a closed genus-0 triangle mesh V = F/2 + 2, tested within a
13// tolerance of F/16. That is a real check and it is NECESSARY, NOT SUFFICIENT, for two reasons the
14// tooth itself cannot see. First, Euler is a SINGLE SCALAR over the whole surface, so compensating
15// defects cancel: punch a hole and add a handle and the characteristic is unchanged. Second, it is
16// computed on ONE subject inside ONE gate, so it is a tooth rather than a ruler -- no other mesh in
17// the estate is ever asked. This organ counts the actual incidences instead, which is the direct
18// measurement Euler is a proxy for.
19//
20// EVERY DEFECT CLASS IS COUNTED SEPARATELY AND CARRIES A WORKLIST. A single is-it-broken boolean is
21// not actionable, and two defect classes with opposite remedies must never share a counter: a
22// boundary edge means the surface is OPEN, a three-face edge means it is NON-MANIFOLD, and an
23// inconsistently wound pair means the surface is CLOSED but its normals disagree. Those are three
24// different repairs. Each class reports its count AND the first offenders by index, and says so when
25// that list is a PREFIX rather than the whole set.
26//
27// PURE INTEGER, NO FLOAT ANYWHERE. Coordinates are caller-defined integer units (micrometres is the
28// estate's habit). The area test is an exact integer cross product, so a degenerate triangle is
29// PROVEN degenerate rather than declared so by an epsilon somebody picked.
30// license_tier: ORIGINAL No hardware writes (Rule 26). Reads buffers, opens nothing.
31import "nx_syscalls.nx"
32
33const MC_I64: i64 = 8
34const MC_LIST_MAX: i64 = 32 // offenders NAMED per class; beyond this the list is a declared PREFIX
35
36// A triangle-key packs three vertex indices into one i64 as ((lo*nv)+mid)*nv+hi, so nv is bounded by
37// the cube root of the i64 range. DERIVED, not picked: 2097151^3 < 2^63. Above it the duplicate-face
38// check REFUSES rather than silently skipping, because a check that quietly does not run is worse
39// than one that says it could not.
40const MC_TRIKEY_MAX_NV: i64 = 2097151
41
42// ---- verdicts ------------------------------------------------------------------------------------
43const MC_CLEAN: i64 = 0
44const MC_DEFECTS: i64 = 1
45const MC_UNMEASURABLE: i64 = 3 // no faces or no vertices: an empty mesh is not a clean mesh
46const MC_INVALID: i64 = 4 // an index out of range: nothing further can be trusted
47
48// ---- out slots -----------------------------------------------------------------------------------
49const MC_O_VERTS: i64 = 0
50const MC_O_TRIS: i64 = 1
51const MC_O_EDGES: i64 = 2 // distinct undirected edges
52const MC_O_BADINDEX: i64 = 3 // triangles referencing a vertex outside [0,nv)
53const MC_O_DEGEN_TOPO: i64 = 4 // a triangle naming the same vertex twice
54const MC_O_DEGEN_AREA: i64 = 5 // three distinct vertices that are collinear: exact zero cross product
55const MC_O_LOOSE: i64 = 6 // vertices no triangle references
56const MC_O_BOUNDARY: i64 = 7 // edges with exactly ONE incident face: the surface is open here
57const MC_O_NONMANIFOLD: i64 = 8 // edges with THREE OR MORE incident faces
58const MC_O_WINDING: i64 = 9 // two-face edges whose faces traverse it the SAME way
59const MC_O_DUPFACE: i64 = 10 // two triangles over the same three vertices
60const MC_O_EULER: i64 = 11 // V - E + F, REPORTED not judged: a proxy, and it is why this organ exists
61const MC_O_DUPCHECKED: i64 = 12 // 1 when the duplicate-face check actually ran, 0 when nv exceeded its bound
62const MC_O_LIST_PREFIX: i64 = 13 // 1 when at least one worklist below is a prefix of its own count
63const MC_O_SLOTS: i64 = 14
64
65// worklist buffer layout: MC_LIST_MAX entries per class, classes in the order
66// BADINDEX, DEGEN_TOPO, DEGEN_AREA, LOOSE, BOUNDARY, NONMANIFOLD, WINDING, DUPFACE
67const MC_L_BADINDEX: i64 = 0
68const MC_L_DEGEN_TOPO: i64 = 1
69const MC_L_DEGEN_AREA: i64 = 2
70const MC_L_LOOSE: i64 = 3
71const MC_L_BOUNDARY: i64 = 4
72const MC_L_NONMANIFOLD: i64 = 5
73const MC_L_WINDING: i64 = 6
74const MC_L_DUPFACE: i64 = 7
75const MC_L_CLASSES: i64 = 8
76
77func mc_list_slots() -> i64 { return MC_L_CLASSES * MC_LIST_MAX }
78
79func mc_list_put(list: *i64, klass: i64, count_so_far: i64, value: i64) -> i64 {
80 if count_so_far < MC_LIST_MAX { list[klass * MC_LIST_MAX + count_so_far] = value; return 1 }
81 return 0
82}
83func mc_list_get(list: *i64, klass: i64, i: i64) -> i64 { return list[klass * MC_LIST_MAX + i] }
84
85// ---- bottom-up merge sort over (key, payload) ----------------------------------------------------
86// O(n log n) DELIBERATELY. A mesh here can be the 662k-triangle BodyParts3D skeleton, which is about
87// two million directed edges; an insertion sort would be a quadratic hang that reads to the caller as
88// a crash. The estate has banked that exact confusion -- an unbounded scan that never returns is
89// indistinguishable from a dead process -- so the complexity is chosen from the data size, not taste.
90func mc_msort(k: *i64, d: *i64, tk: *i64, td: *i64, n: i64) -> i64 {
91 var width: i64 = 1
92 while width < n {
93 var i: i64 = 0
94 while i < n {
95 let lo: i64 = i
96 var mid: i64 = i + width
97 var hi: i64 = i + width + width
98 if mid > n { mid = n }
99 if hi > n { hi = n }
100 var a: i64 = lo
101 var b: i64 = mid
102 var o: i64 = lo
103 while o < hi {
104 var take_a: i64 = 0
105 if a < mid {
106 if b >= hi { take_a = 1 }
107 else { if k[a] <= k[b] { take_a = 1 } }
108 }
109 if take_a == 1 { tk[o] = k[a]; td[o] = d[a]; a = a + 1 }
110 else { tk[o] = k[b]; td[o] = d[b]; b = b + 1 }
111 o = o + 1
112 }
113 i = i + width + width
114 }
115 var c: i64 = 0
116 while c < n { k[c] = tk[c]; d[c] = td[c]; c = c + 1 }
117 width = width + width
118 }
119 return 0
120}
121
122func mc_min(a: i64, b: i64) -> i64 { if a < b { return a } return b }
123func mc_max(a: i64, b: i64) -> i64 { if a > b { return a } return b }
124
125// ---- THE SWEEP -----------------------------------------------------------------------------------
126// verts: 3 integer coords per vertex. tris: 3 vertex indices per triangle.
127// Returns MC_CLEAN / MC_DEFECTS / MC_UNMEASURABLE / MC_INVALID.
128func mc_check(verts: *i64, nv: i64, tris: *i64, nt: i64, out: *i64, list: *i64) -> i64 {
129 var z: i64 = 0
130 while z < MC_O_SLOTS { out[z] = 0; z = z + 1 }
131 z = 0
132 while z < mc_list_slots() { list[z] = 0 - 1; z = z + 1 }
133 out[MC_O_VERTS] = nv
134 out[MC_O_TRIS] = nt
135 // AN EMPTY MESH IS NOT A CLEAN MESH. Returning CLEAN for zero faces would let a generator that
136 // emitted nothing pass the integrity sweep -- the empty-set-passes defect, wearing geometry.
137 if nt <= 0 { return MC_UNMEASURABLE }
138 if nv <= 0 { return MC_UNMEASURABLE }
139
140 // ---- PASS 1: index validity FIRST. Every later pass dereferences these indices, so validating
141 // them afterwards would mean reading out of bounds to discover they were out of bounds.
142 var bad: i64 = 0
143 var t: i64 = 0
144 while t < nt {
145 var c: i64 = 0
146 var t_bad: i64 = 0
147 while c < 3 {
148 let vi: i64 = tris[t * 3 + c]
149 if vi < 0 { t_bad = 1 }
150 if vi >= nv { t_bad = 1 }
151 c = c + 1
152 }
153 if t_bad == 1 {
154 if mc_list_put(list, MC_L_BADINDEX, bad, t) == 0 { out[MC_O_LIST_PREFIX] = 1 }
155 bad = bad + 1
156 }
157 t = t + 1
158 }
159 out[MC_O_BADINDEX] = bad
160 if bad > 0 { return MC_INVALID }
161
162 // ---- PASS 2: degeneracy, and the two kinds are NOT merged.
163 // A triangle naming one vertex twice is a TOPOLOGICAL degeneracy -- the mesh data is wrong.
164 // Three distinct vertices that happen to be collinear is a GEOMETRIC degeneracy -- the data is
165 // well-formed and the shape has zero area. Same symptom on screen, different repair, so they get
166 // different counters. The area test is an EXACT integer cross product: no epsilon, no float, so a
167 // zero here is proven rather than thresholded.
168 var dtopo: i64 = 0
169 var darea: i64 = 0
170 t = 0
171 while t < nt {
172 let ia: i64 = tris[t * 3]
173 let ib: i64 = tris[t * 3 + 1]
174 let ic: i64 = tris[t * 3 + 2]
175 var topo: i64 = 0
176 if ia == ib { topo = 1 }
177 if ib == ic { topo = 1 }
178 if ia == ic { topo = 1 }
179 if topo == 1 {
180 if mc_list_put(list, MC_L_DEGEN_TOPO, dtopo, t) == 0 { out[MC_O_LIST_PREFIX] = 1 }
181 dtopo = dtopo + 1
182 }
183 else {
184 let ux: i64 = verts[ib * 3] - verts[ia * 3]
185 let uy: i64 = verts[ib * 3 + 1] - verts[ia * 3 + 1]
186 let uz: i64 = verts[ib * 3 + 2] - verts[ia * 3 + 2]
187 let wx: i64 = verts[ic * 3] - verts[ia * 3]
188 let wy: i64 = verts[ic * 3 + 1] - verts[ia * 3 + 1]
189 let wz: i64 = verts[ic * 3 + 2] - verts[ia * 3 + 2]
190 let cx: i64 = uy * wz - uz * wy
191 let cy: i64 = uz * wx - ux * wz
192 let cz: i64 = ux * wy - uy * wx
193 if cx == 0 { if cy == 0 { if cz == 0 {
194 if mc_list_put(list, MC_L_DEGEN_AREA, darea, t) == 0 { out[MC_O_LIST_PREFIX] = 1 }
195 darea = darea + 1
196 } } }
197 }
198 t = t + 1
199 }
200 out[MC_O_DEGEN_TOPO] = dtopo
201 out[MC_O_DEGEN_AREA] = darea
202
203 // ---- PASS 3: loose vertices. A vertex no triangle references is invisible, ships in every file,
204 // and is the single most common reason a generated mesh is larger than the shape it draws.
205 let used: *i64 = sys_mmap(nv * MC_I64) as *i64
206 var v: i64 = 0
207 while v < nv { used[v] = 0; v = v + 1 }
208 t = 0
209 while t < nt {
210 used[tris[t * 3]] = 1
211 used[tris[t * 3 + 1]] = 1
212 used[tris[t * 3 + 2]] = 1
213 t = t + 1
214 }
215 var loose: i64 = 0
216 v = 0
217 while v < nv {
218 if used[v] == 0 {
219 if mc_list_put(list, MC_L_LOOSE, loose, v) == 0 { out[MC_O_LIST_PREFIX] = 1 }
220 loose = loose + 1
221 }
222 v = v + 1
223 }
224 out[MC_O_LOOSE] = loose
225 sys_munmap(used as *u8, nv * MC_I64)
226
227 // ---- PASS 4: edge incidence. THIS IS THE MEASUREMENT EULER IS A PROXY FOR.
228 // Every directed edge is keyed by its UNDIRECTED identity (lo*nv+hi) and carries the direction it
229 // was traversed. Sorting by key groups the faces that share each edge, and the run length is the
230 // incidence count: 1 = boundary, 2 = manifold, 3+ = non-manifold. Within a 2-run, two faces that
231 // traversed the edge the SAME way disagree about which side is outside -- inconsistent winding,
232 // which renders as a black or inside-out patch and which a deviation ruler cannot see at all.
233 let ne: i64 = nt * 3
234 let ekey: *i64 = sys_mmap(ne * MC_I64) as *i64
235 let edat: *i64 = sys_mmap(ne * MC_I64) as *i64
236 let tk: *i64 = sys_mmap(ne * MC_I64) as *i64
237 let td: *i64 = sys_mmap(ne * MC_I64) as *i64
238 var e: i64 = 0
239 t = 0
240 while t < nt {
241 var c: i64 = 0
242 while c < 3 {
243 var d: i64 = c + 1
244 if d == 3 { d = 0 }
245 let u: i64 = tris[t * 3 + c]
246 let w: i64 = tris[t * 3 + d]
247 let lo: i64 = mc_min(u, w)
248 let hi: i64 = mc_max(u, w)
249 ekey[e] = lo * nv + hi
250 // payload: direction bit (1 = traversed low to high) times nt, plus the face index, so one
251 // slot carries both without a second array
252 var dir: i64 = 0
253 if u == lo { dir = 1 }
254 edat[e] = dir * nt + t
255 e = e + 1
256 c = c + 1
257 }
258 t = t + 1
259 }
260 mc_msort(ekey, edat, tk, td, ne)
261 var distinct: i64 = 0
262 var boundary: i64 = 0
263 var nonman: i64 = 0
264 var wind: i64 = 0
265 var i: i64 = 0
266 while i < ne {
267 // RUN SCAN WITH AN EXPLICIT FLAG, NEVER A SENTINEL WRITTEN INTO THE CURSOR. The first version
268 // of this loop exited by assigning j = ne + ne and then tried to recover the real end with
269 // j = j - ne, which silently yields exactly ne for EVERY run -- so all three thousand directed
270 // edges read as one enormous run and the clean tetrahedron reported ITSELF non-manifold. That
271 // is this estate's own banked anti-pattern, named in the standing law: A LOOP-EXIT SENTINEL
272 // WRITTEN INTO THE SEARCH CURSOR ERASES THE ANSWER. It was caught on the gate's FIRST run, and
273 // caught in the most useful possible form -- gv_bite reported FALSE-POSITIVE: fired on the good
274 // input, which points straight at the detector rather than at the fixture.
275 var j: i64 = i + 1
276 var run: i64 = 1
277 var scanning: i64 = 1
278 while scanning == 1 {
279 if j >= ne { scanning = 0 }
280 else {
281 if ekey[j] == ekey[i] { run = run + 1; j = j + 1 }
282 else { scanning = 0 }
283 }
284 }
285 distinct = distinct + 1
286 if run == 1 {
287 if mc_list_put(list, MC_L_BOUNDARY, boundary, ekey[i]) == 0 { out[MC_O_LIST_PREFIX] = 1 }
288 boundary = boundary + 1
289 }
290 if run == 2 {
291 let d0: i64 = edat[i] / nt
292 let d1: i64 = edat[i + 1] / nt
293 if d0 == d1 {
294 if mc_list_put(list, MC_L_WINDING, wind, ekey[i]) == 0 { out[MC_O_LIST_PREFIX] = 1 }
295 wind = wind + 1
296 }
297 }
298 if run > 2 {
299 if mc_list_put(list, MC_L_NONMANIFOLD, nonman, ekey[i]) == 0 { out[MC_O_LIST_PREFIX] = 1 }
300 nonman = nonman + 1
301 }
302 i = j
303 }
304 out[MC_O_EDGES] = distinct
305 out[MC_O_BOUNDARY] = boundary
306 out[MC_O_NONMANIFOLD] = nonman
307 out[MC_O_WINDING] = wind
308 sys_munmap(ekey as *u8, ne * MC_I64)
309 sys_munmap(edat as *u8, ne * MC_I64)
310 sys_munmap(tk as *u8, ne * MC_I64)
311 sys_munmap(td as *u8, ne * MC_I64)
312
313 // ---- PASS 5: duplicate faces, or an HONEST REFUSAL. The key packs three indices into one i64,
314 // which bounds nv at the cube root of the range. Above that this check REPORTS THAT IT DID NOT RUN
315 // rather than returning a comfortable zero -- a check that silently skips is indistinguishable
316 // from a check that passed, and this organ exists because that confusion ships defects.
317 var dup: i64 = 0
318 if nv <= MC_TRIKEY_MAX_NV {
319 out[MC_O_DUPCHECKED] = 1
320 let fkey: *i64 = sys_mmap(nt * MC_I64) as *i64
321 let fdat: *i64 = sys_mmap(nt * MC_I64) as *i64
322 let fk2: *i64 = sys_mmap(nt * MC_I64) as *i64
323 let fd2: *i64 = sys_mmap(nt * MC_I64) as *i64
324 t = 0
325 while t < nt {
326 let a0: i64 = tris[t * 3]
327 let b0: i64 = tris[t * 3 + 1]
328 let c0: i64 = tris[t * 3 + 2]
329 var lo: i64 = mc_min(a0, mc_min(b0, c0))
330 var hi: i64 = mc_max(a0, mc_max(b0, c0))
331 let mid: i64 = a0 + b0 + c0 - lo - hi
332 fkey[t] = (lo * nv + mid) * nv + hi
333 fdat[t] = t
334 t = t + 1
335 }
336 mc_msort(fkey, fdat, fk2, fd2, nt)
337 var q: i64 = 1
338 while q < nt {
339 if fkey[q] == fkey[q - 1] {
340 if mc_list_put(list, MC_L_DUPFACE, dup, fdat[q]) == 0 { out[MC_O_LIST_PREFIX] = 1 }
341 dup = dup + 1
342 }
343 q = q + 1
344 }
345 sys_munmap(fkey as *u8, nt * MC_I64)
346 sys_munmap(fdat as *u8, nt * MC_I64)
347 sys_munmap(fk2 as *u8, nt * MC_I64)
348 sys_munmap(fd2 as *u8, nt * MC_I64)
349 }
350 out[MC_O_DUPFACE] = dup
351
352 // EULER IS REPORTED, NEVER JUDGED. V - E + F equals 2 for a closed genus-0 surface, and this organ
353 // publishes it so the number the older tooth tests remains visible -- but it is a single scalar
354 // over the whole surface and compensating defects cancel inside it, which is exactly why the
355 // incidence counts above exist. Reading this field as a verdict would reinstate the proxy.
356 out[MC_O_EULER] = nv - distinct + nt
357
358 if dtopo > 0 { return MC_DEFECTS }
359 if darea > 0 { return MC_DEFECTS }
360 if loose > 0 { return MC_DEFECTS }
361 if nonman > 0 { return MC_DEFECTS }
362 if wind > 0 { return MC_DEFECTS }
363 if dup > 0 { return MC_DEFECTS }
364 // A BOUNDARY EDGE IS DELIBERATELY NOT A DEFECT. An open surface is a legitimate thing to author --
365 // a plane, a cloth patch, a cut-away anatomical section. Watertightness is a SEPARATE question and
366 // gets its own predicate below, so callers who need it ask for it rather than having it imposed.
367 return MC_CLEAN
368}
369
370// Watertight = closed AND manifold AND consistently wound. Asked separately, on purpose.
371func mc_watertight(out: *i64) -> i64 {
372 if out[MC_O_BOUNDARY] != 0 { return 0 }
373 if out[MC_O_NONMANIFOLD] != 0 { return 0 }
374 if out[MC_O_WINDING] != 0 { return 0 }
375 return 1
376}
377
378// Total defects across every class that IS a defect -- boundary excluded for the reason above.
379func mc_defect_total(out: *i64) -> i64 {
380 return out[MC_O_DEGEN_TOPO] + out[MC_O_DEGEN_AREA] + out[MC_O_LOOSE]
381 + out[MC_O_NONMANIFOLD] + out[MC_O_WINDING] + out[MC_O_DUPFACE]
382}