← Commit history

Delete skills/wiki-readme-3d/SKILL.md

John Lauer ·189c254f78 ·3mo ago ·parent fb5a586
1 file changed −148
skills/wiki-readme-3d/SKILL.mddeleted−148
@@ -1,148 +0,0 @@-----name: wiki-readme-3d-description: >-  Put a WORKING interactive 3D viewer (or any data-hungry JS app) inside an Adom Wiki readme.html,-  despite the readme sandbox CSP that blocks ALL fetch (connect-src none), all wasm, and blob:-  images. Covers the complete proven recipe: extend the wiki's own Adom Babylon viewer bundle (never-  roll your own), load GLBs with zero network via File objects, smuggle full-quality models through-  the img-src channel (GLB bytes packed into lossless repo PNGs read back via CORS canvas), the-  blob-to-data-URI texture shim, and the five things the bundle only does inside its own loadModel-  (render loop, lights, camera framing, clip planes, scene clearing). Every rule here failed live-  first on adom/bq25792-charger (2026-07-15). Trigger words: 3d in readme, readme viewer, readme.html-  3d, babylon in readme, model toggle wiki page, readme csp, connect-src none, readme too large,-  interactive readme, glb in readme, exploded view toggle, custom wiki 3d viewer.------# Interactive 3D inside a wiki readme.html (the proven recipe)--Built and verified live on `adom/bq25792-charger` (exploded/assembled stackup toggle). The readme-runs in `<iframe sandbox="allow-scripts allow-downloads">` with CSP:-`default-src 'none'; script-src 'unsafe-inline' https:; style-src 'unsafe-inline' https:;-img-src data: https:; connect-src 'none'; ...` - so: scripts and styles load from https, images-load from https/data, and **nothing else moves: no fetch/XHR (not even data: URIs), no wasm-('wasm-unsafe-eval' absent), no blob: images, no workers.** Plus the `/readme` endpoint refuses-files over **~4-5 MB** ("README too large to render"). Everything below exists to win inside that-box.--## Rule 0: extend the wiki's own viewer - never roll your own--The bundle the wiki itself uses is at-`/static/vendor/adom-3d-viewer-babylon9/adom-3d-viewer-babylon9.esm.js` (+ `style.css`), loadable-from the readme (script-src https:). It sets `window.Adom3DViewerBabylon9` and `window.BABYLON`.-Wiring, copied from the wiki's own `/viewer/3d/...` page:--```js-const viewer = Adom3DViewerBabylon9.init(el, { zUp:true, showViewCube:true, showGround:true });-// public API: loadModel(url), clearScene(), frameModel(), getScene(), getEngine(), getCamera(), toggleAxes()-```--`viewer.loadModel(url)` fetches -> dead under connect-src 'none'. So append into ITS scene yourself:--```js-const f = new File([bytes], "model.glb", { type: "model/gltf-binary" });-await BABYLON.SceneLoader.AppendAsync("", f, viewer.getScene(), null, ".glb");-```--A **File** rides FileReader (not fetch) - CSP-exempt. The explicit `".glb"` plugin hint is required-(data:/blob: URL strings all fail; the wrapper also chokes on non-strings - `lastIndexOf is not a-function`).--## Getting the model BYTES with no network: the PNG image channel--Embedding base64 in the HTML works but hits the ~4-5 MB readme cap fast. The loophole that removes-the cap AND keeps full quality: **img-src allows https:, `/blob/` serves PNG inline with-`access-control-allow-origin: *`, and a CORS-clean canvas is readable.** So pack the GLB bytes into-a lossless PNG in the page repo and decode in the readme:--- Pack (server side): 4-byte LE length header + raw GLB bytes into **RGB channels only** (alpha-  stays 255 - canvas 2D premultiplies alpha, so bytes stored under alpha<255 corrupt). Width 2048,-  height as needed, PIL `Image.frombytes('RGB', ...)`, save PNG (PNG is lossless).-- Decode (readme): `new Image()` with `crossOrigin="anonymous"`, draw to canvas, `getImageData`,-  take bytes 0-2 of every pixel, read the length header, slice. Then the File/AppendAsync load above.--Result on the BQ page: readme.html fell from 1.89 MB (embedded, quality-crushed) to **13 KB**, with-**zero-simplification** models served from the repo.--## Models must be QUANTIZED, never draco (and never gpu-instanced)--- **Draco cannot decode in the sandbox** (wasm blocked, workers blocked, eval blocked). Use-  `gltf-transform optimize --compress quantize` - `KHR_mesh_quantization` is core Babylon/three-  support, zero decoder, and faster first paint than draco anywhere.-- Measured (BQ board, from ~20 MB raw): draco 956 KB | quantize-only (full quality) 4.36 MB |-  quantize+simplify 0.1% 2.4 MB | quantize+simplify 1% 845 KB. **1% simplification is visibly bad-  on PCB detail (John rejected it)** - with the PNG channel there is no size pressure, so ship-  quantize-only full quality.-- **`--instance false` is mandatory**: the optimizer's `EXT_mesh_gpu_instancing` renders as ONE-  giant mis-scaled instance in this Babylon build (a single machine pin filled the screen).--## Textures: the blob-to-data-URI shim--Babylon hands glTF textures to `<img>` via `URL.createObjectURL(blob)` -> blocked (img-src has no-blob:). Even textures stored as data: URIs inside the glTF get blob-ized. Shim once, before loading:--```js-const orig = URL.createObjectURL.bind(URL);-URL.createObjectURL = (b) => (b && b.__dataUri) || orig(b);-const OrigBlob = window.Blob;-window.Blob = function(parts, opts){-  const bl = new OrigBlob(parts, opts);-  try {-    if (parts && parts.length===1 && (parts[0] instanceof Uint8Array || parts[0] instanceof ArrayBuffer)) {-      const u8 = parts[0] instanceof Uint8Array ? parts[0] : new Uint8Array(parts[0]);-      if (u8.length < 3000000) {-        let s=""; for (let i=0;i<u8.length;i+=0x8000) s+=String.fromCharCode.apply(null,u8.subarray(i,i+0x8000));-        bl.__dataUri = "data:"+((opts&&opts.type)||"application/octet-stream")+";base64,"+btoa(s);-      }-    }-  } catch(e){}-  return bl;-};-window.Blob.prototype = OrigBlob.prototype;-```--img-src data: is allowed, so the stamped data: URI renders. (Also convert the GLB's bufferView-images to data: URIs in its glTF JSON - belt and suspenders.)--## The five things the bundle only does inside its own loadModel (do them yourself)--Bypassing `loadModel` means the viewer is mounted but inert. After every append:--1. **Start the render loop** (else the canvas is pure black at "60fps"):-   `eng.stopRenderLoop(); eng.runRenderLoop(()=>scene.render());`-2. **Add lights** (PBR renders as a black silhouette with zero lights and no environment):-   hemispheric + key + fill `DirectionalLight`s is enough.-3. **Frame the camera on `scene.activeCamera`** - `viewer.getCamera()` can be a DIFFERENT object;-   set target/radius on `scene.activeCamera`, inside a `setTimeout(...,200)` so nothing internally-   reframes over you. Use `scene.getWorldExtends()` (full scene - quantized models carry their scale-   in node matrices, per-mesh bounds lie).-4. **Scale the clip planes**: default `minZ` 0.1 CLIPS a small board entirely. Set-   `minZ = diag*0.005`, `maxZ = diag*200`, `wheelPrecision = 3/diag`.-5. **Track and dispose your own appends on toggle** - `viewer.clearScene()` does not know about-   externally appended meshes (they accumulate): snapshot `scene.meshes.length` before append, keep-   the slice, `mesh.dispose(false,true)` each before the next load.--## Verification (headless puppeteer, since pup/CDP screenshots of nested WebGL lie)--- `page.evaluate`: read your status element + `EngineStore.Instances[0].scenes[0].meshes.length`.-- Pixel truth: `readPixels` INSIDE `scene.onAfterRenderObservable` (outside the frame, non--  preserveDrawingBuffer canvases read black). If `onAfterRender` never fires, the render loop-  isn't running - see item 1.-- Note a nonblack-pixel test can be fooled by the clear color - verify the MODEL visually via an-  OS-level window capture (`desktop_screenshot_window`) on a real desktop.--## Dead ends (do not retry)--- `viewer.loadModel("/blob/...")`, `fetch()`, XHR, `fetch("data:...")` - connect-src 'none' blocks all.-- data:/blob: URL strings into loadModel or AppendAsync - loader fetches them; blocked.-- Draco/meshopt GLBs - wasm + workers + eval all blocked.-- Nesting the wiki's `/viewer/3d/...` page in an iframe (frame-src https: allows it, and the scene-  runs at 60fps) - but the nested canvas composites BLACK on screen. Unresolved; avoid.-- Embedding >4-5 MB in readme.html - "README too large to render".-- Serving JS/HTML from `/blob` - `application/octet-stream` + `content-disposition: attachment` +-  nosniff; scripts will not execute from there.--## Platform asks that obsolete parts of this skill (file/track on adom/wiki)--`connect-src 'self'` on `/readme` would replace the PNG channel with a plain fetch; `img-src blob:`-would remove the texture shim; correct content-types on `/blob` (html/js/svg/csv) would allow real-runtime assets. Until then, this recipe is the way.