code wiki / _hdl_build / nx_mesh2glb.nx
nx_mesh2glb.nx source
↩ module page · 338 lines · 15086 B
1// nx_mesh2glb.nx -- NXMSH2 -> INTERACTIVE .glb (operator 2026-08-05: "a picture vs the other files'
2// full interactivity panel -- that's dumb"). The measurement organs emit NXMSH2 (per-tri colour = the
3// deviation heatmap lives there); the showcase viewer consumes .glb. This organ closes that seam, so a
4// FITTED body and a PAINTED HEATMAP both become drag-to-rotate panels, not screenshots -- an interactive
5// deviation map is a capability the reference industrial tools ship as static report pages.
6// Positions PASS THROUGH as raw f32 bits (zero re-quantisation); per-tri colour is replicated to the
7// tri's 3 vertices as normalized u8 VEC4; per-vertex normals transcode to normalized i16 -- the exact
8// accessor types our proven nishi_walk.glb already uses, so the viewer needs nothing new.
9// nx_mesh2glb <in.nxmesh> <out.glb> | selftest
10// license_tier: ORIGINAL expect_exit: 0
11import "nx_syscalls.nx"
12const MG_MAGIC_65536: i64 = 65536
13const MG_MAGIC_32767: i64 = 32767
14const MG_MAGIC_65535: i64 = 65535
15const MG_MAGIC_100000: i64 = 100000
16const MG_MAGIC_8192: i64 = 8192
17
18const MG_CAP: i64 = 33554432
19const MG_OCAP: i64 = 25165824
20const MG_MAXTRI: i64 = 400000
21const MG_M8388607: i64 = 8388607
22const MG_M8388608: i64 = 8388608
23const MG_BIG: i64 = 4611686018427387903
24
25func hw(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
26func pn(v: i64) -> i64 { let b: *u8=sys_mmap(32) as *u8; var x: i64=v; var ng: i64=0; if x<0{ng=1;x=0-x} var i: i64=31; if x==0{b[i]=48 as u8;i=i-1} while x>0{b[i]=(48+x%10) as u8;x=x/10;i=i-1} if ng==1{b[i]=45 as u8;i=i-1} sys_write(1,(b as i64+i+1) as *u8,31-i); return 0 }
27func mg_u32(b: *u8, o: i64) -> i64 { return (b[o] as i64) + ((b[o+1] as i64)<<8) + ((b[o+2] as i64)<<16) + ((b[o+3] as i64)<<24) }
28func mg_w8(b: *u8, o: i64, v: i64) -> i64 { b[o] = (v&255) as u8; return o+1 }
29func mg_w16(b: *u8, o: i64, v: i64) -> i64 { b[o]=(v&255) as u8; b[o+1]=((v>>8)&255) as u8; return o+2 }
30func mg_w32(b: *u8, o: i64, v: i64) -> i64 { b[o]=(v&255) as u8; b[o+1]=((v>>8)&255) as u8; b[o+2]=((v>>16)&255) as u8; b[o+3]=((v>>24)&255) as u8; return o+4 }
31func mg_align4(x: i64) -> i64 { return (x+3)/4*4 }
32func mg_cat(o: *u8, at: i64, s: *u8) -> i64 { var i: i64=0; var a: i64=at; while s[i]!=(0 as u8){o[a]=s[i]; a=a+1; i=i+1} return a }
33func mg_num(o: *u8, at: i64, v: i64) -> i64 {
34 let b: *u8 = sys_mmap(32) as *u8
35 var x: i64 = v
36 var ng: i64 = 0
37 if x < 0 { ng = 1; x = 0-x }
38 var i: i64 = 31
39 if x == 0 { b[i]=48 as u8; i=i-1 }
40 while x > 0 { b[i]=(48+x%10) as u8; x=x/10; i=i-1 }
41 if ng == 1 { b[i]=45 as u8; i=i-1 }
42 var a: i64 = at
43 var j: i64 = i+1
44 while j <= 31 { o[a] = b[j]; a=a+1; j=j+1 }
45 return a
46}
47// f32 bits -> scaled integer (value * scale), for min/max compare and permille decode
48func mg_f32i(w: i64, scale: i64) -> i64 {
49 let sign: i64 = (w >> 31) & 1
50 let expo: i64 = (w >> 23) & 255
51 if expo == 0 { return 0 }
52 var mant: i64 = (w & MG_M8388607) | MG_M8388608
53 let sh: i64 = expo - 127
54 var v: i64 = 0
55 if sh >= 23 { if sh - 23 > 30 { return 0 } }
56 if sh >= 23 { v = mant * scale * (1 << (sh - 23)) }
57 if sh < 23 { if 23 - sh > 62 { return 0 } }
58 if sh < 23 { v = (mant * scale) >> (23 - sh) }
59 if sign == 1 { return 0 - v }
60 return v
61}
62func mg_refuse(reason: *u8) -> i64 { hw("MESH2GLB REFUSED: " as *u8); hw(reason); hw("\n" as *u8); return 0 }
63
64func mg_convert(inp: *u8, outp: *u8) -> i64 {
65 let fd: i64 = sys_openat_rd(inp)
66 if fd < 0 { mg_refuse("input unreadable" as *u8); return 3 }
67 let b: *u8 = sys_mmap(MG_CAP + 64)
68 var n: i64 = 0
69 var go: i64 = 1
70 while go == 1 {
71 let r: i64 = sys_read(fd, ((b as i64) + n) as *u8, MG_CAP - n)
72 if r <= 0 { go = 0 } else { n = n + r }
73 if n >= MG_CAP { go = 0 }
74 }
75 sys_close(fd)
76 if n < 44 { mg_refuse("too small for NXMSH2" as *u8); return 3 }
77 if b[0] != (78 as u8) { mg_refuse("not NXMSH2" as *u8); return 3 }
78 if b[5] != (50 as u8) { mg_refuse("not NXMSH2 v2" as *u8); return 3 }
79 let nlay: i64 = mg_u32(b, 8)
80 let nt: i64 = mg_u32(b, 12)
81 if nt <= 0 { mg_refuse("no triangles" as *u8); return 3 }
82 if nt > MG_MAXTRI { mg_refuse("over tri cap" as *u8); return 3 }
83 let hdr: i64 = 16 + nlay*24
84 if hdr + nt*84 > n { mg_refuse("truncated triangle records" as *u8); return 3 }
85 let nv: i64 = nt*3
86 let ni: i64 = nt*3
87 // ---- BIN layout ----
88 let posLen: i64 = nv*12
89 let nrmOff: i64 = posLen
90 let nrmLen: i64 = nv*6
91 let colOff: i64 = mg_align4(nrmOff + nrmLen)
92 let colLen: i64 = nv*4
93 let idxOff: i64 = colOff + colLen
94 let idxLen: i64 = ni*4
95 let binLen: i64 = mg_align4(idxOff + idxLen)
96 let need: i64 = binLen + MG_MAGIC_65536
97 if need > MG_OCAP { mg_refuse("mesh exceeds the output budget -- decimate first, never truncate silently" as *u8); return 4 }
98 let bin: *u8 = sys_mmap(need)
99 // track min/max position BITS per axis (compare in decoded um space)
100 let mnb: *i64 = sys_mmap(48) as *i64
101 let mxb: *i64 = sys_mmap(48) as *i64
102 let mnv: *i64 = sys_mmap(48) as *i64
103 let mxv: *i64 = sys_mmap(48) as *i64
104 var a2: i64 = 0
105 while a2 < 3 { mnv[a2] = MG_BIG; mxv[a2] = 0-MG_BIG; mnb[a2] = 0; mxb[a2] = 0; a2 = a2 + 1 }
106 var t: i64 = 0
107 var at: i64 = 0
108 while t < nt {
109 var c: i64 = 0
110 while c < 3 {
111 var ax: i64 = 0
112 while ax < 3 {
113 let w: i64 = mg_u32(b, hdr + t*84 + c*12 + ax*4)
114 at = mg_w32(bin, at, w)
115 let dv: i64 = mg_f32i(w, 1000)
116 if dv < mnv[ax] { mnv[ax] = dv; mnb[ax] = w }
117 if dv > mxv[ax] { mxv[ax] = dv; mxb[ax] = w }
118 ax = ax + 1
119 }
120 c = c + 1
121 }
122 t = t + 1
123 }
124 // normals: per-vertex f32 permille -> normalized i16 (v*32767/1000)
125 t = 0
126 at = nrmOff
127 while t < nt {
128 var c2: i64 = 0
129 while c2 < 3 {
130 var ax2: i64 = 0
131 while ax2 < 3 {
132 let w2: i64 = mg_u32(b, hdr + t*84 + 36 + c2*12 + ax2*4)
133 var s16: i64 = mg_f32i(w2, MG_MAGIC_32767) / 1000
134 if s16 > MG_MAGIC_32767 { s16 = MG_MAGIC_32767 }
135 if s16 < 0-MG_MAGIC_32767 { s16 = 0-MG_MAGIC_32767 }
136 at = mg_w16(bin, at, s16 & MG_MAGIC_65535)
137 ax2 = ax2 + 1
138 }
139 c2 = c2 + 1
140 }
141 t = t + 1
142 }
143 // colors: per-TRI f32 permille -> u8 x3 verts, alpha 255; zero colour falls back to bone so the
144 // panel is never invisible (the meshview lesson, inherited deliberately)
145 t = 0
146 at = colOff
147 while t < nt {
148 var cr: i64 = mg_f32i(mg_u32(b, hdr + t*84 + 72), 255) / 1000
149 var cg: i64 = mg_f32i(mg_u32(b, hdr + t*84 + 76), 255) / 1000
150 var cb: i64 = mg_f32i(mg_u32(b, hdr + t*84 + 80), 255) / 1000
151 if cr + cg + cb < 12 { cr = 216; cg = 210; cb = 198 }
152 if cr > 255 { cr = 255 }
153 if cg > 255 { cg = 255 }
154 if cb > 255 { cb = 255 }
155 var c3: i64 = 0
156 while c3 < 3 {
157 at = mg_w8(bin, at, cr)
158 at = mg_w8(bin, at, cg)
159 at = mg_w8(bin, at, cb)
160 at = mg_w8(bin, at, 255)
161 c3 = c3 + 1
162 }
163 t = t + 1
164 }
165 // indices 0..nv-1
166 t = 0
167 at = idxOff
168 while t < ni { at = mg_w32(bin, at, t); t = t + 1 }
169 // ---- JSON ----
170 let json: *u8 = sys_mmap(MG_MAGIC_65536)
171 var jj: i64 = 0
172 jj = mg_cat(json, jj, "{\x22asset\x22:{\x22version\x22:\x222.0\x22,\x22generator\x22:\x22nishi nx_mesh2glb\x22},\x22scene\x22:0,\x22scenes\x22:[{\x22nodes\x22:[0]}],\x22nodes\x22:[{\x22mesh\x22:0}]," as *u8)
173 jj = mg_cat(json, jj, "\x22meshes\x22:[{\x22primitives\x22:[{\x22attributes\x22:{\x22POSITION\x22:0,\x22NORMAL\x22:1,\x22COLOR_0\x22:2},\x22indices\x22:3,\x22material\x22:0}]}]," as *u8)
174 jj = mg_cat(json, jj, "\x22materials\x22:[{\x22pbrMetallicRoughness\x22:{\x22metallicFactor\x22:0,\x22roughnessFactor\x22:1},\x22doubleSided\x22:true}]," as *u8)
175 jj = mg_cat(json, jj, "\x22accessors\x22:[{\x22bufferView\x22:0,\x22componentType\x22:5126,\x22count\x22:" as *u8)
176 jj = mg_num(json, jj, nv)
177 jj = mg_cat(json, jj, ",\x22type\x22:\x22VEC3\x22,\x22min\x22:[" as *u8)
178 // min/max as decoded integers (mm truncation of um) -- viewers use these only for framing
179 jj = mg_num(json, jj, mnv[0]/1000)
180 jj = mg_cat(json, jj, "," as *u8)
181 jj = mg_num(json, jj, mnv[1]/1000)
182 jj = mg_cat(json, jj, "," as *u8)
183 jj = mg_num(json, jj, mnv[2]/1000)
184 jj = mg_cat(json, jj, "],\x22max\x22:[" as *u8)
185 jj = mg_num(json, jj, mxv[0]/1000 + 1)
186 jj = mg_cat(json, jj, "," as *u8)
187 jj = mg_num(json, jj, mxv[1]/1000 + 1)
188 jj = mg_cat(json, jj, "," as *u8)
189 jj = mg_num(json, jj, mxv[2]/1000 + 1)
190 jj = mg_cat(json, jj, "]}," as *u8)
191 jj = mg_cat(json, jj, "{\x22bufferView\x22:1,\x22componentType\x22:5122,\x22normalized\x22:true,\x22count\x22:" as *u8)
192 jj = mg_num(json, jj, nv)
193 jj = mg_cat(json, jj, ",\x22type\x22:\x22VEC3\x22}," as *u8)
194 jj = mg_cat(json, jj, "{\x22bufferView\x22:2,\x22componentType\x22:5121,\x22normalized\x22:true,\x22count\x22:" as *u8)
195 jj = mg_num(json, jj, nv)
196 jj = mg_cat(json, jj, ",\x22type\x22:\x22VEC4\x22}," as *u8)
197 jj = mg_cat(json, jj, "{\x22bufferView\x22:3,\x22componentType\x22:5125,\x22count\x22:" as *u8)
198 jj = mg_num(json, jj, ni)
199 jj = mg_cat(json, jj, ",\x22type\x22:\x22SCALAR\x22}]," as *u8)
200 jj = mg_cat(json, jj, "\x22bufferViews\x22:[{\x22buffer\x22:0,\x22byteOffset\x22:0,\x22byteLength\x22:" as *u8)
201 jj = mg_num(json, jj, posLen)
202 jj = mg_cat(json, jj, "},{\x22buffer\x22:0,\x22byteOffset\x22:" as *u8)
203 jj = mg_num(json, jj, nrmOff)
204 jj = mg_cat(json, jj, ",\x22byteLength\x22:" as *u8)
205 jj = mg_num(json, jj, nrmLen)
206 jj = mg_cat(json, jj, "},{\x22buffer\x22:0,\x22byteOffset\x22:" as *u8)
207 jj = mg_num(json, jj, colOff)
208 jj = mg_cat(json, jj, ",\x22byteLength\x22:" as *u8)
209 jj = mg_num(json, jj, colLen)
210 jj = mg_cat(json, jj, "},{\x22buffer\x22:0,\x22byteOffset\x22:" as *u8)
211 jj = mg_num(json, jj, idxOff)
212 jj = mg_cat(json, jj, ",\x22byteLength\x22:" as *u8)
213 jj = mg_num(json, jj, idxLen)
214 jj = mg_cat(json, jj, "}]," as *u8)
215 jj = mg_cat(json, jj, "\x22buffers\x22:[{\x22byteLength\x22:" as *u8)
216 jj = mg_num(json, jj, binLen)
217 jj = mg_cat(json, jj, "}]}" as *u8)
218 let jsonPad: i64 = mg_align4(jj)
219 // ---- GLB assembly ----
220 let total: i64 = 12 + 8 + jsonPad + 8 + binLen
221 let out: *u8 = sys_mmap(total + 64)
222 var o: i64 = 0
223 o = mg_cat(out, o, "glTF" as *u8)
224 o = mg_w32(out, o, 2)
225 o = mg_w32(out, o, total)
226 o = mg_w32(out, o, jsonPad)
227 o = mg_cat(out, o, "JSON" as *u8)
228 var q: i64 = 0
229 while q < jj { out[o] = json[q]; o = o + 1; q = q + 1 }
230 while q < jsonPad { out[o] = 32 as u8; o = o + 1; q = q + 1 }
231 o = mg_w32(out, o, binLen)
232 o = mg_w8(out, o, 66)
233 o = mg_w8(out, o, 73)
234 o = mg_w8(out, o, 78)
235 o = mg_w8(out, o, 0)
236 q = 0
237 while q < binLen { out[o] = bin[q]; o = o + 1; q = q + 1 }
238 let ofd: i64 = sys_openat_wr(outp, 420)
239 if ofd < 0 { mg_refuse("output unwritable" as *u8); return 6 }
240 sys_write(ofd, out, o)
241 sys_close(ofd)
242 hw("{\x22organ\x22:\x22nx_mesh2glb\x22,\x22tris\x22:" as *u8); pn(nt)
243 hw(",\x22verts\x22:" as *u8); pn(nv)
244 hw(",\x22glb_bytes\x22:" as *u8); pn(o)
245 hw(",\x22note\x22:\x22positions pass through as raw f32 bits (zero requantisation); per-tri colour carried -- a painted HEATMAP stays a heatmap in the interactive viewer\x22}\n" as *u8)
246 return 0
247}
248
249// ---- teeth (literal-only fixture, nx_cc 1785936860 discipline) ----
250func st_enc1000(v: i64) -> i64 {
251 if v == 0 { return 0 }
252 var neg: i64 = 0
253 var m: i64 = v
254 if m < 0 { neg = 1; m = 0-m }
255 var e: i64 = 0
256 var num: i64 = m
257 var den: i64 = 1000
258 while num >= den*2 { den = den*2; e = e+1 }
259 while num < den { num = num*2; e = e-1 }
260 let frac: i64 = ((num - den)*MG_M8388608)/den
261 var bits: i64 = ((e+127) << 23) | (frac & MG_M8388607)
262 if neg == 1 { bits = bits | (1<<31) }
263 return bits
264}
265func st_fix(path: *u8) -> i64 {
266 let b: *u8 = sys_mmap(256)
267 b[0]=78 as u8; b[1]=88 as u8; b[2]=77 as u8; b[3]=83 as u8
268 b[4]=72 as u8; b[5]=50 as u8; b[6]=0 as u8; b[7]=0 as u8
269 mg_w32(b, 8, 1)
270 mg_w32(b, 12, 1)
271 var q: i64 = 0
272 while q < 16 { b[16+q] = 0 as u8; q = q + 1 }
273 b[16]=115 as u8
274 mg_w32(b, 32, 0)
275 mg_w32(b, 36, 1)
276 var k: i64 = 0
277 while k < 21 { mg_w32(b, 40 + k*4, 0); k = k + 1 }
278 mg_w32(b, 52, st_enc1000(MG_MAGIC_100000))
279 mg_w32(b, 68, st_enc1000(MG_MAGIC_100000))
280 mg_w32(b, 40+72, st_enc1000(900))
281 mg_w32(b, 40+76, st_enc1000(120))
282 mg_w32(b, 40+80, st_enc1000(80))
283 mg_w32(b, 124, 0)
284 let fd: i64 = sys_openat_wr(path, 420)
285 if fd < 0 { return 0 - 1 }
286 sys_write(fd, b, 128)
287 sys_close(fd)
288 return 0
289}
290func mg_hasstr(b: *u8, n: i64, lit: *u8) -> i64 {
291 var m: i64 = 0
292 while lit[m] != (0 as u8) { m = m + 1 }
293 var i: i64 = 0
294 while i + m <= n {
295 var k: i64 = 0
296 var ok: i64 = 1
297 while k < m { if b[i+k] != lit[k] { ok = 0; k = m } else { k = k + 1 } }
298 if ok == 1 { return 1 }
299 i = i + 1
300 }
301 return 0
302}
303func mg_selftest() -> i64 {
304 var fails: i64 = 0
305 st_fix("/tmp/mg_t.nxmesh" as *u8)
306 hw("T0 convert a 1-tri red fixture -> valid glb with COLOR_0:\n" as *u8)
307 if mg_convert("/tmp/mg_t.nxmesh" as *u8, "/tmp/mg_t.glb" as *u8) != 0 { fails = fails + 1; hw("T0 FAIL convert refused\n" as *u8) } else {
308 let vb: *u8 = sys_mmap(MG_MAGIC_8192)
309 let vfd: i64 = sys_openat_rd("/tmp/mg_t.glb" as *u8)
310 var vn: i64 = 0
311 if vfd >= 0 { vn = sys_read(vfd, vb, MG_MAGIC_8192); sys_close(vfd) }
312 var ok: i64 = 1
313 if vn < 100 { ok = 0 }
314 if vb[0] != (103 as u8) { ok = 0 }
315 if mg_u32(vb, 8) != vn { ok = 0 }
316 if mg_hasstr(vb, vn, "COLOR_0" as *u8) == 0 { ok = 0 }
317 if ok == 1 { hw("T0 PASS header+length+COLOR_0\n" as *u8) } else { fails = fails + 1; hw("T0 FAIL glb invalid\n" as *u8) }
318 }
319 hw("T1 absent input must REFUSE:\n" as *u8)
320 if mg_convert("/tmp/mg_absent_zz.nxmesh" as *u8, "/tmp/mg_x.glb" as *u8) == 0 { fails = fails + 1; hw("T1 FAIL\n" as *u8) } else { hw("T1 PASS\n" as *u8) }
321 if fails == 0 { hw("MESH2GLB-SELFTEST GREEN 2/2\n" as *u8); return 0 }
322 hw("MESH2GLB-SELFTEST RED fails=" as *u8); pn(fails); hw("\n" as *u8)
323 return 1
324}
325
326func main(argc: i64, argv: *i64) -> i64 {
327 if argc < 2 {
328 hw("usage: nx_mesh2glb <in.nxmesh> <out.glb> | selftest\n" as *u8)
329 sys_exit(2)
330 return 2
331 }
332 let a1: *u8 = argv[1] as *u8
333 if a1[0] == (115 as u8) { let rc: i64 = mg_selftest(); sys_exit(rc); return rc }
334 if argc < 3 { hw("usage: nx_mesh2glb <in.nxmesh> <out.glb>\n" as *u8); sys_exit(2); return 2 }
335 let rc2: i64 = mg_convert(argv[1] as *u8, argv[2] as *u8)
336 sys_exit(rc2)
337 return rc2
338}