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}