code wiki / _hdl_build / nx_wasm_explodelab_html.nx
nx_wasm_explodelab_html.nx source
↩ module page · 35 lines · 7930 B
1// nx_wasm_explodelab_html.nx -- SOVEREIGN packager for EXPLODELAB: emits a self-contained .html (wasm
2// base64-embedded) with LOGIC-FREE JS (physlab law: JS passes raw mouse/key bits + blits the framebuffer +
3// displays exported counters) PLUS the BOM side panel injected from the generator's bom.txt (presentation,
4// not logic -- the assembly tree/BOM came from the REAL STEP parse).
5// usage: nx_wasm_explodelab_html <in.wasm> <bom.txt> <out.html>. license_tier: ORIGINAL
6import "nx_syscalls.nx"
7import "nx_base64.nx"
8
9func hw(fd: i64, s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(fd, s, n); return 0 }
10
11func main(argc: i64, argv: *i64) -> i64 {
12 if argc < 4 { hw(2, "usage: nx_wasm_explodelab_html <in.wasm> <bom.txt> <out.html>\n" as *u8); return 2 }
13 let wlen_p: *i64 = sys_mmap(8) as *i64
14 let wbytes: *u8 = sys_read_file(argv[1] as *u8, wlen_p)
15 if (wbytes as i64) == 0 { hw(2, "read wasm failed\n" as *u8); return 3 }
16 let wlen: i64 = wlen_p[0]
17 let blen_p: *i64 = sys_mmap(8) as *i64
18 let bbytes: *u8 = sys_read_file(argv[2] as *u8, blen_p)
19 if (bbytes as i64) == 0 { hw(2, "read bom failed\n" as *u8); return 3 }
20 let b64: *u8 = sys_mmap(wlen * 2 + 64) as *u8
21 let b64len: i64 = b64_encode(wbytes, wlen, b64)
22 let fd: i64 = sys_openat_wr(argv[3] as *u8, 0x1a4)
23 if fd < 0 { hw(2, "open html failed\n" as *u8); return 4 }
24 hw(fd, "<!doctype html><html><head><meta charset='utf-8'><title>Nishi ExplodeLab -- interactive exploded view of a REAL STEP assembly</title><style>html,body{background:#0b0d12;margin:0;height:100%;overflow:hidden;font-family:'Segoe UI',system-ui,sans-serif;color:#cdd6e4}#wrap{display:flex;align-items:center;justify-content:center;height:100vh;width:100vw}canvas{image-rendering:pixelated;width:100vw;height:100vh;object-fit:contain;background:radial-gradient(ellipse at 50% 35%,#1c2230 0%,#12151d 60%,#0b0d12 100%);cursor:grab}#bar{position:fixed;top:0;left:0;right:0;height:44px;display:flex;align-items:center;gap:14px;padding:0 16px;background:linear-gradient(180deg,rgba(13,16,24,.92),rgba(13,16,24,.72));border-bottom:1px solid #232c3f;backdrop-filter:blur(6px)}#brand{font-weight:600;font-size:14px;letter-spacing:.12em;color:#7fd4ff}#brand b{color:#e8eef8}.kc{font-size:11px;color:#9fb2cd}.key{display:inline-block;min-width:16px;text-align:center;padding:1px 5px;margin-right:4px;border:1px solid #33415e;border-bottom-width:2px;border-radius:4px;background:#161b28;color:#cfe3ff;font-family:Consolas,monospace;font-size:11px}#fps{margin-left:auto;font-size:11px;color:#6f83a3;font-family:Consolas,monospace}#bom{position:fixed;top:56px;right:12px;width:320px;background:rgba(13,16,24,.88);border:1px solid #232c3f;border-radius:10px;font-size:12px;color:#bcd;max-height:calc(100vh - 76px);overflow:auto;box-shadow:0 8px 28px rgba(0,0,0,.5);backdrop-filter:blur(6px)}#bom h3{margin:0;padding:10px 14px;font-size:12px;letter-spacing:.08em;color:#7fd4ff;border-bottom:1px solid #232c3f;position:sticky;top:0;background:rgba(13,16,24,.95)}#rows{padding:6px 8px}.row{display:flex;align-items:center;gap:8px;padding:4px 8px;border-radius:6px;cursor:default}.row:hover{background:#1a2233}.row.sel{background:#1f3050;outline:1px solid #3d5a8f}.chip{width:10px;height:10px;border-radius:3px;flex:none;box-shadow:0 0 4px rgba(0,0,0,.6)}.nm{flex:none;color:#e2eaf6;font-weight:500}.dep{flex:none;font-size:10px;color:#5f7397;border:1px solid #2a3550;border-radius:8px;padding:0 6px}.dims{margin-left:auto;font-family:Consolas,monospace;font-size:10.5px;color:#8fa7c9;text-align:right;white-space:nowrap}pre#raw{display:none}</style></head><body><div id='wrap'><canvas id='c'></canvas></div><div id='bar'><span id='brand'>NISHI <b>EXPLODELAB</b></span><span class='kc'><span class='key'>drag</span>orbit</span><span class='kc'><span class='key'>click</span>highlight</span><span class='kc'><span class='key'>E</span>explode</span><span class='kc'><span class='key'>D</span>assemble</span><span class='kc'><span class='key'>R</span>reset</span><span class='kc'><span class='key'>G</span>fullscreen</span><span id='fps'>sovereign wasm · zero JS logic</span></div><div id='bom'><h3>AS1 — REAL STEP ASSEMBLY · 18 PARTS</h3><div id='rows'></div><pre id='raw'>" as *u8)
25 sys_write(fd, bbytes, blen_p[0])
26 hw(fd, "</pre></div><script>\n" as *u8)
27 hw(fd, "const B='" as *u8)
28 sys_write(fd, b64, b64len)
29 hw(fd, "';\nconst fpsEl=document.getElementById('fps');function diag(m){fpsEl.textContent=m;}window.onerror=(m,s,l)=>{diag('JS ERROR: '+m+' @line '+l);};\n" as *u8)
30 hw(fd, "const KC={'nut':'#c96a4e','bolt':'#e6c95a','rod':'#7f9bd4','plate':'#b9a58f','l-bracket':'#e0e6ee'};\nconst raw=document.getElementById('raw').textContent.split('\\n');const rowsEl=document.getElementById('rows');const rowByPart={};\nraw.forEach(L=>{const m=L.match(/^(\\d+)\\s+(\\S+)\\s+\\(depth (\\d+)\\)(?:\\s+--\\s+(.*))?$/);if(m===null){return;}const d=document.createElement('div');d.className='row';d.style.marginLeft=((Number(m[3])-1)*10)+'px';const ch=document.createElement('span');ch.className='chip';ch.style.background=KC[m[2]]||'#8892a6';const nm=document.createElement('span');nm.className='nm';nm.textContent=m[2];const dp=document.createElement('span');dp.className='dep';dp.textContent='d'+m[3];const dm=document.createElement('span');dm.className='dims';dm.textContent=(m[4]||'').replace(' | ','\\u2009|\\u2009');d.appendChild(ch);d.appendChild(nm);d.appendChild(dp);d.appendChild(dm);rowsEl.appendChild(d);rowByPart[Number(m[1])]=d;});\n" as *u8)
31 hw(fd, "const bytes=Uint8Array.from(atob(B),c=>c.charCodeAt(0));\nWebAssembly.instantiate(bytes).then(r=>{const ex=r.instance.exports;const W=Number(ex.ww()),H=Number(ex.hh()),off=Number(ex.fb_off());const cv=document.getElementById('c');cv.width=W;cv.height=H;const ctx=cv.getContext('2d');const img=ctx.createImageData(W,H);const mem=new Uint8Array(ex.memory.buffer);ex.start();let mx=160,my=120,btn=0,frames=0,lastSel=-2;const t0=performance.now();function pos(e){const r2=cv.getBoundingClientRect();mx=Math.max(0,Math.min(W-1,Math.round((e.clientX-r2.left)*W/r2.width)));my=Math.max(0,Math.min(H-1,Math.round((e.clientY-r2.top)*H/r2.height)));}addEventListener('mousemove',pos);cv.addEventListener('mousedown',e=>{e.preventDefault();pos(e);btn|=1;});addEventListener('mouseup',()=>{btn&=~1;});cv.addEventListener('contextmenu',e=>e.preventDefault());addEventListener('keydown',e=>{if(e.code=='KeyR')btn|=2;if(e.code=='KeyE')btn|=4;if(e.code=='KeyD')btn|=8;if(e.code=='KeyG'){if(document.fullscreenElement)document.exitFullscreen();else document.documentElement.requestFullscreen();}});addEventListener('keyup',e=>{if(e.code=='KeyR')btn&=~2;if(e.code=='KeyE')btn&=~4;if(e.code=='KeyD')btn&=~8;});function syncSel(){const s=Number(ex.selected());if(s===lastSel){return;}if(rowByPart[lastSel]){rowByPart[lastSel].classList.remove('sel');}if(rowByPart[s]){rowByPart[s].classList.add('sel');rowByPart[s].scrollIntoView({block:'nearest'});}lastSel=s;}function frame(){try{ex.tick(BigInt(mx),BigInt(my),BigInt(btn));ex.render();for(let i=0;i<W*H;i++){const b=off+i*8;img.data[i*4]=mem[b];img.data[i*4+1]=mem[b+1];img.data[i*4+2]=mem[b+2];img.data[i*4+3]=255;}ctx.putImageData(img,0,0);frames++;if(frames%10===0){syncSel();}if(frames===1){diag(Number(ex.part_count())+' parts · file-true geometry');fpsEl.innerHTML=Number(ex.part_count())+' parts · file-true geometry';}if(frames%240===0){fpsEl.textContent=Math.round(frames*1000/(performance.now()-t0))+' fps · explode '+Number(ex.explode_t())+'/1024 · '+Number(ex.angle())+'°';}}catch(e){diag('SIM ERROR: '+e);return;}requestAnimationFrame(frame);}frame();}).catch(e=>{diag('WASM LOAD ERROR: '+e);});\n</script></body></html>\n" as *u8)
32 sys_close(fd)
33 hw(1, "nx_wasm_explodelab_html: wrote html (logic-free JS + BOM panel)\n" as *u8)
34 return 0
35}