code wiki / (root) / nx_curveskel_lib.nx

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}