nx_curveskel_lib.nx source
↩ module page · 476 lines · 23720 B
1// nx_curveskel_lib.nx -- SOVEREIGN SKELETON-FIT: extract a curve skeleton (an armature) from a BARE unrigged mesh.
2//
3// WHY (2026-09-05). nx_boneheat ships the SKIN half of auto-rig, and its accept rule needs a skeleton to weight
4// against. Blender's Rigify only assembles a CONTROL rig from a meta-rig a human PLACES by hand -- its own manual
5// says it does not attach to a mesh -- and UniRig/RigNet predict a skeleton with a LEARNED model over a training
6// set. Neither is sovereign-from-the-first-byte for an arbitrary mesh. This is the classical route done right:
7// a deterministic integer CURVE SKELETON by geodesic farthest-point peeling, needing no checkpoint and no data.
8// 1. VOXELISE + SEAL the mesh volume -- COMPOSED from nx_boneheat_lib (bh_grid_build/bh_raster/bh_flood), the
9// SAME voxeliser the skin solve uses, so the two halves cannot disagree about the volume. Interior 0 (a
10// sheet with no volume) is REFUSED by name.
11// 2. MEDIAL RADIUS field: multi-source Dijkstra seeded from EVERY surface cell over the domain -> each interior
12// cell's distance to the nearest surface = its local half-thickness. The ROOT is the thickest interior cell.
13// 3. PEEL limbs: seed the current skeleton set, Dijkstra, take the geodesically FARTHEST interior cell as a
14// limb tip, and gradient-descend the geodesic field from the tip back to the skeleton -- that path IS a
15// bone chain. Repeat until the farthest remaining point is closer than the body's own half-thickness (a
16// derived stop, not a magic number: a bump thinner than the torso is not a limb). This is the
17// shortest-path curve-skeleton family (Verroust-Lazarus class), integer and deterministic.
18// 4. SAMPLE joints along each chain spaced by the LOCAL medial radius (thick regions sparse, thin regions
19// dense -- derived from the field), parent-chained, the first joint of a limb parented to the nearest
20// existing joint so branches (a T or a cross) form real fork joints. Emit a SKEL section; nx_boneheat skins it.
21// Safety caps CS_MAX_JOINTS / CS_MAX_LIMBS are NAMED bounds on output size and ANNOUNCE truncation; they are not
22// tuning. UNITS: VERT is 0.01 mm model units (NXA v1); joint positions are cell centres in the same units.
23// license_tier: ORIGINAL
24import "nx_syscalls.nx"
25import "nx_nxa.nx"
26import "nx_boneheat_lib.nx"
27
28const CS_MAX_JOINTS: i64 = 256 // NAMED safety cap on emitted joints; truncation ANNOUNCES, never silent
29const CS_MAX_LIMBS: i64 = 32 // NAMED safety cap on peeled limbs; truncation ANNOUNCES
30const CS_JOINT_WORDS: i64 = 8 // SKEL row: parent tx ty tz qx qy qz qw (shared with nx_boneheat_lib)
31const CS_ROOTPARENT: i64 = 0 - 1
32const CS_TRACE_INF: i64 = 1152921504606846976
33const CS_EXIT_OK: i64 = 0
34const CS_EXIT_USAGE: i64 = 2
35const CS_EXIT_REFUSE: i64 = 3
36// report word offsets (shared vocabulary with the program and the gate)
37const CR_VERTS: i64 = 0
38const CR_TRIS: i64 = 1
39const CR_CELLS: i64 = 2
40const CR_INTERIOR: i64 = 3
41const CR_SEALED: i64 = 4
42const CR_ROOT_R: i64 = 5
43const CR_LIMBS: i64 = 6
44const CR_JOINTS: i64 = 7
45const CR_BRANCHES: i64 = 8
46const CR_MAXPATH: i64 = 9
47const CR_BYTES: i64 = 10
48const CR_TRUNC: i64 = 11
49const CR_NX: i64 = 12
50const CR_NY: i64 = 13
51const CR_NZ: i64 = 14
52const CR_CELL: i64 = 15
53const CR_ROOTCELL: i64 = 16
54const CR_WALLS: i64 = 17 // internal-wall cells bh_flood stripped to INTERIOR before the medial field was built
55const CR_INSIG: i64 = 18 // tips claimed WITHOUT a joint: they reached no farther than the medial radius where they attach
56const CR_PEELS: i64 = 19 // peel iterations (limbs + insignificant tips); CS_MAX_PEELS bounds it and truncation ANNOUNCES
57const CS_MAX_PEELS: i64 = 512 // NAMED safety cap on peel iterations (one Dijkstra over the domain each); announced, never tuning
58const CR_WORDS: i64 = 24
59
60func cs_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
61func cs_num(v: i64) -> i64 {
62 let t: *u8 = sys_mmap(32)
63 var m: i64 = v
64 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m }
65 var k: i64 = 0
66 if m == 0 { t[0] = 48 as u8; k = 1 }
67 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
68 let b: *u8 = sys_mmap(32)
69 var j: i64 = 0
70 while j < k { b[j] = t[k - 1 - j]; j = j + 1 }
71 sys_write(1, b, k)
72 return 0
73}
74func cs_kv(k: *u8, v: i64) -> i64 { cs_puts(k); cs_num(v); return 0 }
75// cell centre in model units for axis component (0=x,1=y,2=z)
76func cs_cell_center(g: *i64, c: i64, axis: i64) -> i64 {
77 let nx: i64 = g[BG_NX]; let ny: i64 = g[BG_NY]
78 let cell: i64 = g[BG_CELL]
79 var ix: i64 = c % nx
80 var iy: i64 = (c / nx) % ny
81 var iz: i64 = c / (nx * ny)
82 if axis == 0 { return g[BG_OX] + ix * cell + cell / 2 }
83 if axis == 1 { return g[BG_OY] + iy * cell + cell / 2 }
84 return g[BG_OZ] + iz * cell + cell / 2
85}
86// seed EVERY surface cell at distance 0 (for the medial radius field)
87func cs_seed_surface(g: *i64) -> i64 {
88 let kind: *u8 = g[BG_KIND] as *u8
89 var n: i64 = 0
90 var c: i64 = 0
91 while c < g[BG_N] {
92 if (kind[c] as i64) == BH_KIND_SURFACE { bh_seed(g, c); n = n + 1 }
93 c = c + 1
94 }
95 return n
96}
97// copy the current dist field out to `dst`
98func cs_copy_dist(g: *i64, dst: *i64) -> i64 {
99 let dist: *i64 = g[BG_DIST] as *i64
100 var c: i64 = 0
101 while c < g[BG_N] { dst[c] = dist[c]; c = c + 1 }
102 return 0
103}
104// interior cell with the maximum value in field `f` (BH_DIST_INF = unreached, skipped); -1 if none
105func cs_argmax_interior(g: *i64, f: *i64) -> i64 {
106 let kind: *u8 = g[BG_KIND] as *u8
107 var best: i64 = 0 - 1
108 var bv: i64 = 0 - 1
109 var c: i64 = 0
110 while c < g[BG_N] {
111 if (kind[c] as i64) == BH_KIND_INTERIOR {
112 let d: i64 = f[c]
113 if d < CS_TRACE_INF { if d > bv { bv = d; best = c } }
114 }
115 c = c + 1
116 }
117 return best
118}
119// seed EVERY skeleton cell (skel[c]==1) at distance 0
120func cs_seed_skel(g: *i64, skel: *u8) -> i64 {
121 var n: i64 = 0
122 var c: i64 = 0
123 while c < g[BG_N] { if (skel[c] as i64) == 1 { if bh_is_domain(g, c) == 1 { bh_seed(g, c); n = n + 1 } } c = c + 1 }
124 return n
125}
126// gradient-descend field D from `start` toward a seed (D==0), appending cells to path[]; returns path length
127func cs_trace(g: *i64, d: *i64, dsurf: *i64, start: i64, path: *i64, cap: i64) -> i64 {
128 let nx: i64 = g[BG_NX]; let ny: i64 = g[BG_NY]
129 var cur: i64 = start
130 var plen: i64 = 0
131 var steps: i64 = 0
132 while steps < g[BG_N] {
133 if plen < cap { path[plen] = cur; plen = plen + 1 }
134 if d[cur] == 0 { break }
135 let cx: i64 = cur % nx
136 let cy: i64 = (cur / nx) % ny
137 let cz: i64 = cur / (nx * ny)
138 // MEDIAL-FOLLOWING descent: among neighbours that descend the geodesic field, step to the THICKEST
139 // (largest medial radius), ties to the steepest. Plain steepest descent hugs the interior wall, its
140 // tube then claims almost nothing, and the body's own bulk peels as false limbs (measured: 9 on a bar).
141 var bestn: i64 = 0 - 1
142 var bestd: i64 = d[cur]
143 var bestr: i64 = 0 - 1
144 var dz: i64 = 0 - 1
145 while dz <= 1 {
146 var dy: i64 = 0 - 1
147 while dy <= 1 {
148 var dx: i64 = 0 - 1
149 while dx <= 1 {
150 if bh_abs(dx) + bh_abs(dy) + bh_abs(dz) > 0 {
151 let m: i64 = bh_idx(g, cx + dx, cy + dy, cz + dz)
152 if m >= 0 { if bh_is_domain(g, m) == 1 { if d[m] < d[cur] {
153 if dsurf[m] > bestr { bestr = dsurf[m]; bestd = d[m]; bestn = m }
154 else { if dsurf[m] == bestr { if d[m] < bestd { bestd = d[m]; bestn = m } } }
155 } } }
156 }
157 dx = dx + 1
158 }
159 dy = dy + 1
160 }
161 dz = dz + 1
162 }
163 if bestn < 0 { break }
164 cur = bestn
165 steps = steps + 1
166 }
167 return plen
168}
169// nearest existing joint (by squared centre distance) to cell c; -1 if none
170func cs_nearest_joint(g: *i64, joints: *i64, nj: i64, c: i64) -> i64 {
171 if nj < 1 { return 0 - 1 }
172 let cxu: i64 = cs_cell_center(g, c, 0)
173 let cyu: i64 = cs_cell_center(g, c, 1)
174 let czu: i64 = cs_cell_center(g, c, 2)
175 var best: i64 = 0
176 var bd: i64 = CS_TRACE_INF
177 var j: i64 = 0
178 while j < nj {
179 let dx: i64 = joints[j * 4 + 1] - cxu
180 let dy: i64 = joints[j * 4 + 2] - cyu
181 let dz: i64 = joints[j * 4 + 3] - czu
182 let d2: i64 = dx * dx + dy * dy + dz * dz
183 if d2 < bd { bd = d2; best = j }
184 j = j + 1
185 }
186 return best
187}
188// the LOCAL medial radius at a cell: the thickest dsurf inside the cube of the cell's own radius, grown until it stops
189// growing. A path cell that runs off the medial axis (a corner tip's descent hugs one side of a block whose medial set is a
190// PLANE) reads a dsurf smaller than the limb's half-width; the thickest neighbour IS the axis cell and its dsurf is the
191// cross-section. One primitive, two users: the claim tube (so a tube covers the whole cross-section -- MEASURED 2026-09-06:
192// a 20x10 block whose path hugged y=+3 left a one-cell sliver at y=-3 that peeled three times under local significance) and
193// the significance bar (a sliver beside a 5 mm leg measures against the leg's 5 mm; a finger beside a hand against the
194// hand's half-thickness). Derived from the field, no constant.
195func cs_local_radius(g: *i64, dsurf: *i64, c: i64) -> i64 {
196 let nx: i64 = g[BG_NX]; let ny: i64 = g[BG_NY]
197 let cx: i64 = c % nx
198 let cy: i64 = (c / nx) % ny
199 let cz: i64 = c / (nx * ny)
200 var best: i64 = dsurf[c]
201 var rad: i64 = 0
202 var grown: i64 = 1
203 while grown == 1 {
204 grown = 0
205 var r2: i64 = best / BH_W_FACE + 1
206 if r2 < 1 { r2 = 1 }
207 if r2 <= rad { break }
208 rad = r2
209 var pz: i64 = 0 - rad
210 while pz <= rad {
211 var py: i64 = 0 - rad
212 while py <= rad {
213 var px: i64 = 0 - rad
214 while px <= rad {
215 let q: i64 = bh_idx(g, cx + px, cy + py, cz + pz)
216 if q >= 0 { if bh_is_domain(g, q) == 1 { if dsurf[q] < CS_TRACE_INF { if dsurf[q] > best { best = dsurf[q]; grown = 1 } } } }
217 px = px + 1
218 }
219 py = py + 1
220 }
221 pz = pz + 1
222 }
223 }
224 return best
225}
226// a bone CLAIMS its cross-section: mark every domain cell within the local medial radius of each path cell as
227// skeleton, so the next peel cannot mistake the body's own thickness for a new limb (the fix for over-peeling a
228// straight bar into dozens of false limbs). The radius is the medial radius at the path cell, derived from the field.
229func cs_claim_tube(g: *i64, skel: *u8, dsurf: *i64, path: *i64, plen: i64) -> i64 {
230 let nx: i64 = g[BG_NX]; let ny: i64 = g[BG_NY]
231 var i: i64 = 0
232 while i < plen {
233 let c: i64 = path[i]
234 // the floor division drops up to one cell of the outermost interior ring; claim that ring too. The radius is the
235 // LOCAL medial radius (the thickest cell in the cross-section), never the path cell's own reading -- see cs_local_radius.
236 var rad: i64 = cs_local_radius(g, dsurf, c) / BH_W_FACE + 1
237 if rad < 1 { rad = 1 }
238 let cx: i64 = c % nx
239 let cy: i64 = (c / nx) % ny
240 let cz: i64 = c / (nx * ny)
241 // (2026-09-06) A RELAXED CLAIM by the thickest cross-section in the first cube was tried here against a biped whose
242 // legs share a plane and REFUTED: the leg still peeled three times. The defect was never the claim -- it was a
243 // zero-thickness internal wall the marcher emits where two parts abut at SDF 0, and bh_flood now strips those.
244 // The claim stays the medial radius at the path cell, so the stop bar and every proven fixture are untouched.
245 var dz: i64 = 0 - rad
246 while dz <= rad {
247 var dy: i64 = 0 - rad
248 while dy <= rad {
249 var dx: i64 = 0 - rad
250 while dx <= rad {
251 let m: i64 = bh_idx(g, cx + dx, cy + dy, cz + dz)
252 if m >= 0 { if bh_is_domain(g, m) == 1 { skel[m] = 1 as u8 } }
253 dx = dx + 1
254 }
255 dy = dy + 1
256 }
257 dz = dz + 1
258 }
259 i = i + 1
260 }
261 return 0
262}
263// the ROOT: the thickest interior cell, ties broken by nearness to the interior centroid. A uniform bar has
264// every axis cell equally thick and first-in-scan-order would root it at one END; the conventional root is the
265// body centre (pelvis/torso), which is also what makes both ends of a bar peel as limbs. Derived, deterministic.
266func cs_root_cell(g: *i64, dsurf: *i64) -> i64 {
267 let nx: i64 = g[BG_NX]; let ny: i64 = g[BG_NY]
268 let kind: *u8 = g[BG_KIND] as *u8
269 var sx: i64 = 0; var sy: i64 = 0; var sz: i64 = 0; var n: i64 = 0
270 var mx: i64 = 0 - 1
271 var c: i64 = 0
272 while c < g[BG_N] {
273 if (kind[c] as i64) == BH_KIND_INTERIOR {
274 sx = sx + c % nx; sy = sy + (c / nx) % ny; sz = sz + c / (nx * ny); n = n + 1
275 if dsurf[c] < CS_TRACE_INF { if dsurf[c] > mx { mx = dsurf[c] } }
276 }
277 c = c + 1
278 }
279 if n < 1 { return 0 - 1 }
280 if mx < 0 { return 0 - 1 }
281 let cxm: i64 = sx / n; let cym: i64 = sy / n; let czm: i64 = sz / n
282 var best: i64 = 0 - 1
283 var bd: i64 = CS_TRACE_INF
284 c = 0
285 while c < g[BG_N] {
286 if (kind[c] as i64) == BH_KIND_INTERIOR { if dsurf[c] == mx {
287 let dx: i64 = c % nx - cxm; let dy: i64 = (c / nx) % ny - cym; let dz: i64 = c / (nx * ny) - czm
288 let d2: i64 = dx * dx + dy * dy + dz * dz
289 if d2 < bd { bd = d2; best = c }
290 } }
291 c = c + 1
292 }
293 return best
294}
295// the run: read, voxelise, medial field, peel limbs, emit SKEL. rep[] carries every count.
296// joints[] packs 4 words per joint: parent, cx, cy, cz (model units).
297func cs_run(inpath: *u8, outpath: *u8, rep: *i64) -> i64 {
298 return cs_run_cells(inpath, outpath, rep, 0)
299}
300// cells_override > 0 is a caller's ladder step (the referee's resolution ladder); 0 means the conf row grid_cells_longest=
301// with the bootstrap default behind it -- argv above conf above default, the estate's configuration order, announced by CR_*.
302func cs_run_cells(inpath: *u8, outpath: *u8, rep: *i64, cells_override: i64) -> i64 {
303 let lp: *i64 = sys_mmap(16) as *i64
304 let b: *u8 = sys_read_file(inpath, lp)
305 if (b as i64) == 0 { cs_puts("CURVESKEL-REFUSE cannot read input\n" as *u8); return CS_EXIT_REFUSE }
306 let flen: i64 = lp[0]
307 let w: *i64 = b as *i64
308 let vwo: i64 = nxa_find(b, flen, nxa_tag4("VERT" as *u8))
309 let two: i64 = nxa_find(b, flen, nxa_tag4("TRIS" as *u8))
310 if vwo < 0 { cs_puts("CURVESKEL-REFUSE no valid VERT section (not an NXA or corrupt)\n" as *u8); return CS_EXIT_REFUSE }
311 if two < 0 { cs_puts("CURVESKEL-REFUSE no valid TRIS section (a point cloud has no volume to skeletonise)\n" as *u8); return CS_EXIT_REFUSE }
312 let nv: i64 = w[vwo]
313 let nt: i64 = w[two]
314 if nv < 1 { cs_puts("CURVESKEL-REFUSE empty VERT\n" as *u8); return CS_EXIT_REFUSE }
315 if nt < 1 { cs_puts("CURVESKEL-REFUSE empty TRIS\n" as *u8); return CS_EXIT_REFUSE }
316 rep[CR_VERTS] = nv; rep[CR_TRIS] = nt
317 var cells: i64 = cells_override
318 if cells < 1 { cells = bh_conf_cells(BH_CONF_PATH) }
319 if cells < 1 { cells = BH_GRID_DEFAULT }
320 let g: *i64 = sys_mmap(BG_WORDS * 8) as *i64
321 bh_grid_build(g, w, vwo, nv, cells)
322 bh_raster(g, w, vwo, two, nt)
323 bh_flood(g)
324 rep[CR_CELLS] = g[BG_N]; rep[CR_INTERIOR] = g[BG_INT]; rep[CR_WALLS] = g[BG_WALLS]
325 rep[CR_NX] = g[BG_NX]; rep[CR_NY] = g[BG_NY]; rep[CR_NZ] = g[BG_NZ]; rep[CR_CELL] = g[BG_CELL]
326 if g[BG_INT] > 0 { rep[CR_SEALED] = 1 } else { rep[CR_SEALED] = 0 }
327 if g[BG_INT] < 1 { cs_puts("CURVESKEL-REFUSE no interior volume (a sheet or an open shell has no medial axis)\n" as *u8); return CS_EXIT_REFUSE }
328 // medial radius field: distance from every surface cell into the interior
329 let dsurf: *i64 = sys_mmap(g[BG_N] * 8 + 64) as *i64
330 bh_reset(g)
331 cs_seed_surface(g)
332 bh_dijkstra(g)
333 cs_copy_dist(g, dsurf)
334 let root: i64 = cs_root_cell(g, dsurf)
335 if root < 0 { cs_puts("CURVESKEL-REFUSE no interior cell reachable from the surface\n" as *u8); return CS_EXIT_REFUSE }
336 rep[CR_ROOT_R] = dsurf[root]
337 rep[CR_ROOTCELL] = root
338 let rootr_cells: i64 = dsurf[root] / BH_W_FACE
339 var stop: i64 = dsurf[root] // DERIVED stop: a limb must reach past the body half-thickness
340 if stop < BH_W_FACE { stop = BH_W_FACE }
341 let skel: *u8 = sys_mmap(g[BG_N] + 8)
342 var c0: i64 = 0
343 while c0 < g[BG_N] { skel[c0] = 0 as u8; c0 = c0 + 1 }
344 skel[root] = 1 as u8
345 let joints: *i64 = sys_mmap(CS_MAX_JOINTS * 4 * 8 + 64) as *i64
346 let childcount: *i64 = sys_mmap(CS_MAX_JOINTS * 8 + 64) as *i64
347 var nj: i64 = 0
348 joints[0] = CS_ROOTPARENT
349 joints[1] = cs_cell_center(g, root, 0); joints[2] = cs_cell_center(g, root, 1); joints[3] = cs_cell_center(g, root, 2)
350 childcount[0] = 0
351 nj = 1
352 let path: *i64 = sys_mmap(g[BG_N] * 8 + 64) as *i64
353 var trunc: i64 = 0
354 var maxpath: i64 = 0
355 var limbs: i64 = 0
356 var peels: i64 = 0
357 var insignificant: i64 = 0
358 while limbs < CS_MAX_LIMBS {
359 bh_reset(g)
360 cs_seed_skel(g, skel)
361 bh_dijkstra(g)
362 let dgeo: *i64 = g[BG_DIST] as *i64
363 let tip: i64 = cs_argmax_interior(g, dgeo)
364 if tip < 0 { break }
365 // nothing farther than one cell from the skeleton remains: every interior cell is claimed or adjacent
366 if dgeo[tip] <= BH_W_FACE { break }
367 let plen: i64 = cs_trace(g, dgeo, dsurf, tip, path, g[BG_N])
368 // LOCAL SIGNIFICANCE (2026-09-06, from the M2 referee): a protrusion is a limb when it reaches farther than the
369 // medial radius WHERE IT ATTACHES, not farther than the body's root radius. The global bar (dsurf[root]) could never
370 // peel a 60 mm finger off a hand on a torso 110 mm thick, and the referee read 461 permil coverage of the artist joints
371 // with fingers, toes and face joints absent by construction. The bar at the root itself is unchanged (the attachment
372 // cell beside the root tube reads the root radius), so every proven fixture keeps its count. An insignificant tip is
373 // CLAIMED (its tube) without a joint so the next farthest cell is a different candidate; CS_MAX_PEELS bounds the loop.
374 let attach: i64 = path[plen - 1]
375 var bar: i64 = cs_local_radius(g, dsurf, attach)
376 if bar < BH_W_FACE { bar = BH_W_FACE }
377 peels = peels + 1
378 var skipit: i64 = 0
379 if dgeo[tip] <= bar { skipit = 1; cs_claim_tube(g, skel, dsurf, path, plen); insignificant = insignificant + 1 }
380 if skipit == 0 {
381 if plen > maxpath { maxpath = plen }
382 // path[0]=tip .. path[plen-1]=adjacent-to-skeleton. Emit attach->tip so parent chains toward the tip.
383 var prev: i64 = cs_nearest_joint(g, joints, nj, path[plen - 1])
384 var acc: i64 = 0
385 var i: i64 = plen - 1
386 while i >= 0 {
387 let cc: i64 = path[i]
388 skel[cc] = 1 as u8
389 acc = acc + 1
390 var spacing: i64 = dsurf[cc] / BH_W_FACE
391 if spacing < 1 { spacing = 1 }
392 if acc >= spacing { if i > 0 {
393 if nj < CS_MAX_JOINTS {
394 joints[nj * 4] = prev
395 joints[nj * 4 + 1] = cs_cell_center(g, cc, 0)
396 joints[nj * 4 + 2] = cs_cell_center(g, cc, 1)
397 joints[nj * 4 + 3] = cs_cell_center(g, cc, 2)
398 childcount[nj] = 0
399 if prev >= 0 { childcount[prev] = childcount[prev] + 1 }
400 prev = nj
401 nj = nj + 1
402 acc = 0
403 } else { trunc = 1 }
404 } }
405 i = i - 1
406 }
407 // the tip itself is always a joint
408 if nj < CS_MAX_JOINTS {
409 joints[nj * 4] = prev
410 joints[nj * 4 + 1] = cs_cell_center(g, tip, 0)
411 joints[nj * 4 + 2] = cs_cell_center(g, tip, 1)
412 joints[nj * 4 + 3] = cs_cell_center(g, tip, 2)
413 childcount[nj] = 0
414 if prev >= 0 { childcount[prev] = childcount[prev] + 1 }
415 nj = nj + 1
416 } else { trunc = 1 }
417 cs_claim_tube(g, skel, dsurf, path, plen)
418 limbs = limbs + 1
419 }
420 if peels >= CS_MAX_PEELS { trunc = 1; break }
421 }
422 if limbs >= CS_MAX_LIMBS { trunc = 1 }
423 rep[CR_INSIG] = insignificant; rep[CR_PEELS] = peels
424 rep[CR_LIMBS] = limbs; rep[CR_JOINTS] = nj; rep[CR_MAXPATH] = maxpath; rep[CR_TRUNC] = trunc
425 // branch joints = joints with >= 2 children (a fork: a T or a cross body joint)
426 var branches: i64 = 0
427 var jb: i64 = 0
428 while jb < nj { if childcount[jb] >= 2 { branches = branches + 1 } jb = jb + 1 }
429 rep[CR_BRANCHES] = branches
430 // emit SKEL (identity bind quats) into the NXA beside the original VERT/TRIS
431 let sk: *i64 = sys_mmap((1 + nj * CS_JOINT_WORDS) * 8 + 64) as *i64
432 sk[0] = nj
433 var j2: i64 = 0
434 while j2 < nj {
435 let o: i64 = 1 + j2 * CS_JOINT_WORDS
436 sk[o] = joints[j2 * 4]
437 sk[o + 1] = joints[j2 * 4 + 1]; sk[o + 2] = joints[j2 * 4 + 2]; sk[o + 3] = joints[j2 * 4 + 3]
438 sk[o + 4] = 0; sk[o + 5] = 0; sk[o + 6] = 0; sk[o + 7] = BH_Q12
439 j2 = j2 + 1
440 }
441 let h: *i64 = b as *i64
442 let ns0: i64 = h[2]
443 let tb: *i64 = ((b as i64) + 32) as *i64
444 let tags: *i64 = sys_mmap((ns0 + 1) * 8 + 64) as *i64
445 let ptrs: *i64 = sys_mmap((ns0 + 1) * 8 + 64) as *i64
446 let wls: *i64 = sys_mmap((ns0 + 1) * 8 + 64) as *i64
447 let skeltag: i64 = nxa_tag4("SKEL" as *u8)
448 var ns: i64 = 0
449 var replaced: i64 = 0
450 var s0: i64 = 0
451 while s0 < ns0 {
452 tags[ns] = tb[s0 * 4]
453 if tags[ns] == skeltag { ptrs[ns] = sk as i64; wls[ns] = 1 + nj * CS_JOINT_WORDS; replaced = 1 }
454 else { ptrs[ns] = (b as i64) + tb[s0 * 4 + 1]; wls[ns] = tb[s0 * 4 + 2] }
455 ns = ns + 1
456 s0 = s0 + 1
457 }
458 if replaced == 0 { tags[ns] = skeltag; ptrs[ns] = sk as i64; wls[ns] = 1 + nj * CS_JOINT_WORDS; ns = ns + 1 }
459 let wrote: i64 = bh_nxa_write(outpath, ns, tags, ptrs, wls)
460 if wrote < 0 { cs_puts("CURVESKEL-REFUSE cannot write output\n" as *u8); return CS_EXIT_REFUSE }
461 rep[CR_BYTES] = wrote
462 return CS_EXIT_OK
463}
464func cs_report(rep: *i64, inpath: *u8, outpath: *u8) -> i64 {
465 cs_puts("CURVESKEL" as *u8)
466 cs_kv(" verts=" as *u8, rep[CR_VERTS]); cs_kv(" tris=" as *u8, rep[CR_TRIS])
467 cs_kv(" grid=" as *u8, rep[CR_NX]); cs_kv("x" as *u8, rep[CR_NY]); cs_kv("x" as *u8, rep[CR_NZ]); cs_kv(" cell=" as *u8, rep[CR_CELL])
468 cs_kv(" cells=" as *u8, rep[CR_CELLS]); cs_kv(" interior=" as *u8, rep[CR_INTERIOR]); cs_kv(" walls=" as *u8, rep[CR_WALLS]); cs_kv(" sealed=" as *u8, rep[CR_SEALED])
469 cs_kv(" root_radius=" as *u8, rep[CR_ROOT_R]); cs_kv(" limbs=" as *u8, rep[CR_LIMBS])
470 cs_kv(" joints=" as *u8, rep[CR_JOINTS]); cs_kv(" branches=" as *u8, rep[CR_BRANCHES])
471 cs_kv(" max_path_cells=" as *u8, rep[CR_MAXPATH]); cs_kv(" truncated=" as *u8, rep[CR_TRUNC])
472 cs_kv(" peels=" as *u8, rep[CR_PEELS]); cs_kv(" insignificant=" as *u8, rep[CR_INSIG])
473 cs_kv(" wrote=" as *u8, rep[CR_BYTES])
474 cs_puts(" in=" as *u8); cs_puts(inpath); cs_puts(" out=" as *u8); cs_puts(outpath); cs_puts("\n" as *u8)
475 return 0
476}