code wiki / (root) / nx_devpaint_lib.nx

nx_devpaint_lib.nx source

↩ module page · 228 lines · 10693 B

1// nx_devpaint_lib.nx -- THE DERIVED-SCALE DEVIATION HEATMAP (2026-08-26). 2// 3// WHY, MEASURED: /exceed/vrm/seed-san.html has been telling visitors that 4// /exceed/vrm/nishi_seedsan_heatmap.glb is "the INTERACTIVE deviation heatmap (drag-rotate the painted 5// gap itself)". That file was BYTE-IDENTICAL to nishi_fitted_seedsan.glb (sha256 1ef8ec04..., 9,435,856 B 6// each): a copy of the unpainted mesh with no deviation on it anywhere. Rule 25 says rewrite it better 7// rather than filter it out, so this lib is the emitter that makes the claim true. 8// 9// WHAT IS NEW HERE, versus nx_meshdist's incumbent `paint` verb, which is otherwise a working heatmap: 10// 1. THE SCALE IS DERIVED FROM THE MEASURED DISTRIBUTION, NOT PASSED IN. md_paint takes `band_um` as 11// an argument and defaults it to 5000 -- a number the caller has to guess, and the published run 12// guessed 10000. A guessed band is a magic number wearing an argument's clothes: pick it too small 13// and every triangle saturates magenta, too large and the whole body is green, and BOTH pictures 14// look authoritative. Here the ramp ceiling is the measured p95 rounded up to the next whole 15// millimetre, and dp_provenance names that rule so the number travels with the picture. 16// 2. IT MEASURES PER VERTEX, NOT PER CENTROID. md_paint takes the distance from each triangle's 17// centroid. A centroid can sit on the reference surface while a corner is far off it, so the 18// incumbent can hide deviation at exactly the sharp features you built a heatmap to find. 19// 3. THE RAMP IS CONTINUOUS, not five hard bands, so a 39 mm and a 41 mm triangle no longer land in 20// different colours because of where a band edge happened to fall. 21// 22// COMPOSED, NEVER RE-IMPLEMENTED: the distance itself is nx_mmdev_lib's exact point-to-triangle ruler 23// (Ericson 5.1.5 + its uniform grid, integer, no sampling). The colour codec is nx_nxmesh_lib's. This 24// file adds the derivation and the ramp and nothing else -- there is no second distance ruler here. 25// 26// DECLARED IMPRECISIONS, all three: 27// (a) NXMSH2 carries ONE colour per TRIANGLE. Three measured corner deviations are therefore reduced 28// to one colour by MAX -- a triangle is as bad as its worst corner. That is wrong in the direction 29// of SHOWING deviation rather than hiding it, which is the safe direction for an inspection tool. 30// Measured on the shipping subject, a triangle spans about 10 mm on a 1.6 m body and renders about 31// 2 px at showcase size, so per-corner interpolation would change little that a viewer can see; 32// the reduction is a format limit, not a shortcut, and lifting it needs a per-vertex colour source 33// in nx_mesh2glb, which is named here rather than silently wished for. 34// (b) Percentiles come from a histogram of MD_HIST_BINS one-tenth-millimetre bins, so they are exact 35// to the bin. Deviations past the last bin land in it and are counted, so p95 can never be 36// reported BELOW a deviation that fell off the end. 37// (c) The scale is per-subject by construction. Two heatmaps are only comparable by reading the 38// printed bounds, which is exactly why dp_provenance exists and why the CLI prints them. 39// license_tier: ORIGINAL 40 41import "nx_syscalls.nx" 42import "nx_vecmath.nx" 43import "nx_nxmesh_lib.nx" 44import "nx_mmdev_lib.nx" 45 46// ---- result slots ------------------------------------------------------------------------------ 47const DP_R_CORNERS: i64 = 0 // triangle-corners measured: the FULL population, never a sample 48const DP_R_TRIS: i64 = 1 49const DP_R_LO: i64 = 2 // ramp floor, ruler units (tenths of a millimetre) 50const DP_R_HI: i64 = 3 // ramp ceiling, DERIVED -- see dp_scale 51const DP_R_P50: i64 = 4 52const DP_R_P95: i64 = 5 53const DP_R_MAX: i64 = 6 54const DP_R_MEAN: i64 = 7 55const DP_R_SATURATED: i64 = 8 // corners at or above HI: they clamp to the top stop 56const DP_R_OVERFLOW: i64 = 9 // corners past the last histogram bin 57const DP_R_REFUSED: i64 = 10 // 0 ok, 1 extent bound, 3 empty 58const DP_R_N: i64 = 11 59 60// ---- the ramp ---------------------------------------------------------------------------------- 61// Five stops, per-mille RGB, byte-for-byte the five band colours nx_meshdist's md_paint already uses. 62// Deliberate: the published page documents its histogram in this vocabulary ("green 211 / yellow 110 / 63// orange 97 / red 77 / magenta 502 permil"), and a new heatmap that reversed or recoloured the ordinal 64// scale would silently invalidate every reading anyone has already taken from the old picture. 65const DP_STOPS: i64 = 5 66const DP_SEGS: i64 = 4 // DERIVED: DP_STOPS - 1 segments between five stops 67const DP_PCT_DEN: i64 = 100 68const DP_P50_NUM: i64 = 50 69const DP_P95_NUM: i64 = 95 70 71func dp_stop_r(i: i64) -> i64 { 72 if i <= 0 { return 100 } 73 if i == 1 { return 900 } 74 if i == 2 { return 950 } 75 if i == 3 { return 920 } 76 return 850 77} 78func dp_stop_g(i: i64) -> i64 { 79 if i <= 0 { return 850 } 80 if i == 1 { return 880 } 81 if i == 2 { return 550 } 82 if i == 3 { return 120 } 83 return 100 84} 85func dp_stop_b(i: i64) -> i64 { 86 if i <= 0 { return 150 } 87 if i == 1 { return 100 } 88 if i == 2 { return 80 } 89 if i == 3 { return 80 } 90 return 850 91} 92 93// deviation d (ruler units) -> per-mille RGB in out3, linearly interpolated between the five stops. 94// d <= lo lands EXACTLY on stop 0 and d >= hi EXACTLY on stop 4, so a mesh compared with itself paints 95// uniformly in the floor colour -- the property the gate's anti-vacuity tooth binds to. 96func dp_ramp(d: i64, lo: i64, hi: i64, out3: *i64) -> i64 { 97 var span: i64 = hi - lo 98 if span < 1 { span = 1 } 99 var x: i64 = d - lo 100 if x < 0 { x = 0 } 101 if x > span { x = span } 102 let u: i64 = x * DP_SEGS 103 var seg: i64 = u / span 104 if seg >= DP_SEGS { seg = DP_SEGS - 1 } 105 let f: i64 = u - seg * span 106 out3[0] = dp_stop_r(seg) + (dp_stop_r(seg+1) - dp_stop_r(seg)) * f / span 107 out3[1] = dp_stop_g(seg) + (dp_stop_g(seg+1) - dp_stop_g(seg)) * f / span 108 out3[2] = dp_stop_b(seg) + (dp_stop_b(seg+1) - dp_stop_b(seg)) * f / span 109 return 0 110} 111 112func dp_res() -> *i64 { 113 let r: *i64 = sys_mmap(DP_R_N * 8) as *i64 114 var i: i64 = 0 115 while i < DP_R_N { r[i] = 0; i = i + 1 } 116 return r 117} 118 119// first bin at which the cumulative count reaches num/den of the population 120func dp_pct(hist: *i64, cnt: i64, num: i64, den: i64) -> i64 { 121 let target: i64 = cnt * num / den 122 var acc: i64 = 0 123 var out: i64 = MD_HIST_BINS - 1 124 var found: i64 = 0 125 var i: i64 = 0 126 while i < MD_HIST_BINS { 127 if found == 0 { 128 acc = acc + hist[i] 129 if acc >= target { out = i; found = 1 } 130 } 131 i = i + 1 132 } 133 return out 134} 135 136// Per-CORNER deviation of every triangle of `ca` to the surface of `ref`, into dev[nt*3]. 137// Full population: every corner of every triangle, and DP_R_CORNERS prints the count so the gate can 138// bind its assertions to it. Returns the refusal code (0 ok). 139func dp_measure(ca: *u8, ref: *u8, dev: *i64, res: *i64) -> i64 { 140 let gp: *i64 = sys_mmap(MD_G_WORDS * 8) as *i64 141 let grc: i64 = md_grid_build(ref, gp) 142 if grc != 0 { res[DP_R_REFUSED] = grc; return grc } 143 let nt: i64 = nm_ntris(ca) 144 if nt <= 0 { res[DP_R_REFUSED] = 3; return 3 } 145 let tb: i64 = nm_tri_base(ca) 146 let hist: *i64 = sys_mmap(MD_HIST_BINS * 8) as *i64 147 var i: i64 = 0 148 while i < MD_HIST_BINS { hist[i] = 0; i = i + 1 } 149 var sum: i64 = 0 150 var mx: i64 = 0 151 var cnt: i64 = 0 152 var t: i64 = 0 153 while t < nt { 154 var v: i64 = 0 155 while v < 3 { 156 let d2: i64 = md_query(ref, gp, nm_coord(ca, tb, t, v, 0), nm_coord(ca, tb, t, v, 1), nm_coord(ca, tb, t, v, 2)) 157 let d: i64 = vm_isqrt(d2) 158 dev[t*3 + v] = d 159 sum = sum + d 160 if d > mx { mx = d } 161 var bin: i64 = d 162 if bin >= MD_HIST_BINS { bin = MD_HIST_BINS - 1; res[DP_R_OVERFLOW] = res[DP_R_OVERFLOW] + 1 } 163 hist[bin] = hist[bin] + 1 164 cnt = cnt + 1 165 v = v + 1 166 } 167 t = t + 1 168 } 169 res[DP_R_TRIS] = nt 170 res[DP_R_CORNERS] = cnt 171 res[DP_R_MAX] = mx 172 res[DP_R_MEAN] = sum / cnt 173 res[DP_R_P50] = dp_pct(hist, cnt, DP_P50_NUM, DP_PCT_DEN) 174 res[DP_R_P95] = dp_pct(hist, cnt, DP_P95_NUM, DP_PCT_DEN) 175 return 0 176} 177 178// THE DERIVATION. lo is 0 because zero deviation must map to the zero colour -- that is what makes a 179// mesh compared against itself paint uniformly in the floor colour, and it is not a free parameter. 180// hi is the measured p95 rounded UP to the next whole millimetre: 181// p95, because the ramp should spend its resolution on the surface that exists. Using max instead 182// lets a single outlier -- a stray hair triangle 300 mm from the body -- flatten everything else 183// into the floor colour and report a flattering picture of a bad fit. 184// whole millimetre, because the millimetre IS this ruler's clinical unit; the rounding quantum comes 185// from nx_nxmesh_lib's own unit ladder (NM_UNIT_PER_FILE_UNIT ruler units per mm), not from taste. 186// FLOOR: one millimetre. When the two meshes agree to within the ramp's own quantum, p95 rounds to 0 187// and the span would be zero. Clamping to one millimetre keeps the arithmetic defined AND paints the 188// whole mesh in the floor colour, which is the truthful picture of two meshes that agree. 189func dp_scale(res: *i64) -> i64 { 190 res[DP_R_LO] = 0 191 let q: i64 = NM_UNIT_PER_FILE_UNIT 192 var hi: i64 = (res[DP_R_P95] + q - 1) / q * q 193 if hi < q { hi = q } 194 res[DP_R_HI] = hi 195 return hi 196} 197 198// Count the corners that clamp at the top of the ramp -- published so a reader can see how much of the 199// surface the scale is saturating rather than resolving. 200func dp_saturated(dev: *i64, ncorners: i64, hi: i64) -> i64 { 201 var n: i64 = 0 202 var i: i64 = 0 203 while i < ncorners { 204 if dev[i] >= hi { n = n + 1 } 205 i = i + 1 206 } 207 return n 208} 209 210// The MAX-OF-CORNERS reduction named in declared imprecision (a) above. 211func dp_tri_dev(dev: *i64, t: i64) -> i64 { 212 var d: i64 = dev[t*3] 213 if dev[t*3+1] > d { d = dev[t*3+1] } 214 if dev[t*3+2] > d { d = dev[t*3+2] } 215 return d 216} 217 218// Paint every triangle of the buffer in place through nx_nxmesh_lib's colour codec. 219func dp_paint(buf: *u8, tri_base: i64, nt: i64, dev: *i64, lo: i64, hi: i64) -> i64 { 220 let rgb: *i64 = sys_mmap(NM_COL_CHANNELS * 8) as *i64 221 var t: i64 = 0 222 while t < nt { 223 dp_ramp(dp_tri_dev(dev, t), lo, hi, rgb) 224 nm_col_put(buf, tri_base, t, rgb[0], rgb[1], rgb[2]) 225 t = t + 1 226 } 227 return nt 228}