nx_curveskel_lib.nx
buildroot/runtime/nx_curveskel_lib.nx
about
nx_curveskel_lib.nx -- SOVEREIGN SKELETON-FIT: extract a curve skeleton (an armature) from a BARE unrigged mesh.
WHY (2026-09-05). nx_boneheat ships the SKIN half of auto-rig, and its accept rule needs a skeleton to weight
against. Blender's Rigify only assembles a CONTROL rig from a meta-rig a human PLACES by hand -- its own manual
says it does not attach to a mesh -- and UniRig/RigNet predict a skeleton with a LEARNED model over a training
set. Neither is sovereign-from-the-first-byte for an arbitrary mesh. This is the classical route done right:
a deterministic integer CURVE SKELETON by geodesic farthest-point peeling, needing no checkpoint and no data.
1. VOXELISE + SEAL the mesh volume -- COMPOSED from nx_boneheat_lib (bh_grid_build/bh_raster/bh_flood), the
SAME voxeliser the skin solve uses, so the two halves cannot disagree about the volume. Interior 0 (a
sheet with no volume) is REFUSED by name.
2. MEDIAL RADIUS field: multi-source Dijkstra seeded from EVERY surface cell over the domain -> each interior
cell's distance to the nearest surface = its local half-thickness. The ROOT is the thickest interior cell.
3. PEEL limbs: seed the current skeleton set, Dijkstra, take the geodesically FARTHEST interior cell as a
limb tip, and gradient-descend the geodesic field from the tip back to the skeleton -- that path IS a
bone chain. Repeat until the farthest remaining point is closer than the body's own half-thickness (a
derived stop, not a magic number: a bump thinner than the torso is not a limb). This is the
shortest-path curve-skeleton family (Verroust-Lazarus class), integer and deterministic.
4. SAMPLE joints along each chain spaced by the LOCAL medial radius (thick regions sparse, thin regions
dense -- derived from the field), parent-chained, the first joint of a limb parented to the nearest
existing joint so branches (a T or a cross) form real fork joints. Emit a SKEL section; nx_boneheat skins it.
Safety caps CS_MAX_JOINTS / CS_MAX_LIMBS are NAMED bounds on output size and ANNOUNCE truncation; they are not
tuning. UNITS: VERT is 0.01 mm model units (NXA v1); joint positions are cell centres in the same units.
license_tier: ORIGINAL
dependencies 3 imports · 6 importers
imports: nx_syscalls.nxnx_nxa.nxnx_boneheat_lib.nx
imported by: nx_autorig_mesh.nxnx_autorig_pose_gate.nxnx_curveskel.nxnx_curveskel_gate.nxnx_rigbench_lib.nxnx_rigbench_lib_t21.nx
structs
| none |
consts
| 28 | const CS_MAX_JOINTS: i64 = 256 // NAMED safety cap on emitted joints; truncation ANNOUNCES, never silent |
| 29 | const CS_MAX_LIMBS: i64 = 32 // NAMED safety cap on peeled limbs; truncation ANNOUNCES |
| 30 | const CS_JOINT_WORDS: i64 = 8 // SKEL row: parent tx ty tz qx qy qz qw (shared with nx_boneheat_lib) |
| 31 | const CS_ROOTPARENT: i64 = 0 - 1 |
| 32 | const CS_TRACE_INF: i64 = 1152921504606846976 |
| 33 | const CS_EXIT_OK: i64 = 0 |
| 34 | const CS_EXIT_USAGE: i64 = 2 |
| 35 | const CS_EXIT_REFUSE: i64 = 3 |
| 37 | const CR_VERTS: i64 = 0 |
| 38 | const CR_TRIS: i64 = 1 |
| 39 | const CR_CELLS: i64 = 2 |
| 40 | const CR_INTERIOR: i64 = 3 |
| 41 | const CR_SEALED: i64 = 4 |
| 42 | const CR_ROOT_R: i64 = 5 |
| 43 | const CR_LIMBS: i64 = 6 |
| 44 | const CR_JOINTS: i64 = 7 |
| 45 | const CR_BRANCHES: i64 = 8 |
| 46 | const CR_MAXPATH: i64 = 9 |
| 47 | const CR_BYTES: i64 = 10 |
| 48 | const CR_TRUNC: i64 = 11 |
| 49 | const CR_NX: i64 = 12 |
| 50 | const CR_NY: i64 = 13 |
| 51 | const CR_NZ: i64 = 14 |
| 52 | const CR_CELL: i64 = 15 |
| 53 | const CR_ROOTCELL: i64 = 16 |
| 54 | const CR_WALLS: i64 = 17 // internal-wall cells bh_flood stripped to INTERIOR before the medial field was built |
| 55 | const CR_INSIG: i64 = 18 // tips claimed WITHOUT a joint: they reached no farther than the medial radius where they attach |
| 56 | const CR_PEELS: i64 = 19 // peel iterations (limbs + insignificant tips); CS_MAX_PEELS bounds it and truncation ANNOUNCES |
| 57 | const CS_MAX_PEELS: i64 = 512 // NAMED safety cap on peel iterations (one Dijkstra over the domain each); announced, never tuning |
| 58 | const CR_WORDS: i64 = 24 |
functions
| 60 | func 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 } |
| 61 | func cs_num(v: i64) -> i64 |
| 74 | func cs_kv(k: *u8, v: i64) -> i64 { cs_puts(k); cs_num(v); return 0 } |
| 76 | func cs_cell_center(g: *i64, c: i64, axis: i64) -> i64 |
| 87 | func cs_seed_surface(g: *i64) -> i64 |
| 98 | func cs_copy_dist(g: *i64, dst: *i64) -> i64 called by 1: cs_run_cells |
| 105 | func cs_argmax_interior(g: *i64, f: *i64) -> i64 called by 1: cs_run_cells |
| 120 | func cs_seed_skel(g: *i64, skel: *u8) -> i64 |
| 127 | func cs_trace(g: *i64, d: *i64, dsurf: *i64, start: i64, path: *i64, cap: i64) -> i64 |
| 170 | func cs_nearest_joint(g: *i64, joints: *i64, nj: i64, c: i64) -> i64 |
| 195 | func cs_local_radius(g: *i64, dsurf: *i64, c: i64) -> i64 |
| 229 | func cs_claim_tube(g: *i64, skel: *u8, dsurf: *i64, path: *i64, plen: i64) -> i64 |
| 266 | func cs_root_cell(g: *i64, dsurf: *i64) -> i64 called by 1: cs_run_cells |
| 297 | func cs_run(inpath: *u8, outpath: *u8, rep: *i64) -> i64 |
| 302 | func cs_run_cells(inpath: *u8, outpath: *u8, rep: *i64, cells_override: i64) -> i64 called by 3: cs_runrb_runrb_run calls 22: sys_mmapsys_read_filecs_putsnxa_findnxa_tag4bh_conf_cells+16 |
| 464 | func cs_report(rep: *i64, inpath: *u8, outpath: *u8) -> i64 |