# Renderer STEP conversion evidence ## Environment - Browser route tested: `http://127.0.0.1:49177/viewer/body-f-chest-v4` - Fixture path tested: `public/models/EInk_click_v101.step` served as `/models/EInk_click_v101.step` - Local tools observed: Node `v24.15.0`, npm `11.12.1`, `wasm-bindgen 0.2.121`, `dx 0.7.9`, no `emcc` in PATH. ## Commands run ```sh just fix cargo check -p polymodel --target wasm32-unknown-unknown --features web cargo check --workspace just check just build-renderer-worker node tools/design-detector/detect.mjs --json assets/styling/viewer.css node tools/renderer-step/prepare-full-npm-control.mjs node tools/renderer-step/measure-assets.mjs cargo tree -p polymodel --target wasm32-unknown-unknown --features web --edges normal dx serve --addr 127.0.0.1 --port 49177 --interactive false --watch false --hot-reload false --open false ``` `cargo check --workspace` and `just check` emitted an unrelated `jacquard-identity` dead-code warning for `invalidate_authority_chain`; checks still passed. ## Asset sizes measured Measured with `node tools/renderer-step/measure-assets.mjs` after staging the single-threaded custom `opencascade.yml` Docker build via `just build-opencascade-step`: | asset | raw bytes | gzip bytes | note | | --- | ---: | ---: | --- | | `public/vendor/opencascade/opencascade.js` | 134,503 | 34,346 | single-thread custom JS staged from build output | | `public/vendor/opencascade/opencascade.wasm` | 6,128,224 | 2,719,991 | single-thread custom WASM staged from build output | | `public/vendor/opencascade/opencascade.worker.js` | missing | missing | expected for single-thread build | | `public/step_worker_loader.js` | 300 | 230 | committed loader | | `public/step_worker.js` | 7,449 | 2,222 | committed worker harness | Earlier control measurement of published `opencascade.js@1.1.1` was 65.9 MB raw / 13.7 MB gzip WASM and lacked the needed XDE/GLB bindings. The custom build is much smaller and exposes the required conversion path. ## Browser observations Before clicking **Convert STEP fixture**, the browser fetched the Dioxus app and renderer worker assets. It did **not** fetch `step_worker_loader.js`, `step_worker.js`, `/models/EInk_click_v101.step`, or `/vendor/opencascade/*`. After clicking **Convert STEP fixture**, Playwright network output showed lazy fetches: ```text GET /step_worker_loader.js 200 GET /step_worker.js 200 GET /models/EInk_click_v101.step 200 GET /vendor/opencascade/opencascade.js 200 GET /vendor/opencascade/opencascade.wasm 200 ``` No browser-console error came from the STEP worker. The only console errors observed were Dioxus dev-server websocket messages caused by running with hot reload disabled: ```text WebSocket connection to 'ws://127.0.0.1:49177/_dioxus?build_id=0' failed: net::ERR_CONNECTION_REFUSED ``` The page reported `crossOriginIsolated=false`. The single-threaded custom build does not need COOP/COEP or a pthread worker sidecar. ## Conversion result The single-threaded custom OpenCascade.js build initialized inside the browser STEP worker, fetched `EInk_click_v101.step`, converted it to GLB bytes, and posted a `converted` result back to the Dioxus app. The app passes the converted GLB object URL into the retained renderer session as `MeshFormat::Gltf`; the renderer worker loads and renders it successfully. Small electronics/PCB STEP fixtures need camera near-plane and orbit-distance settings that scale with the model bounds. Validation run after the handoff and camera-fit fixes: ```sh just check ``` `just check` passed, including the app check and `polymodel-renderer-worker` wasm check. ### Earlier full-npm-control failures The published full npm control initialized inside a browser Worker far enough to validate bindings, fetch the STEP fixture, and load the 65.9 MB OCCT WASM. It could not perform the target STEP→GLB pipeline because the published package lacks required XDE/GLB bindings for this workflow. Observed worker failures while tightening the harness: ```text STEP worker blocked after 1325 ms: Message_ProgressRange_1 is missing from this OpenCascade.js build; crossOriginIsolated=false. STEP worker blocked after 4350 ms: TDF_LabelSequence_1 is missing from this OpenCascade.js build; crossOriginIsolated=false. ``` The final worker harness fails fast with a required-binding inventory, so future runs report the missing custom-build surface directly rather than one symbol at a time. ## Dependency separation The app crate does not depend on OCCT or the STEP worker through Cargo. The wasm app dependency-tree check showed only: ```text polymodel ├── polymodel-api ├── polymodel-renderer-protocol ``` and no matches for `three-d`, `three-d-asset`, `stl_io`, `polymodel-mesh`, or `opencascade` in the app tree. OCCT assets are runtime-loaded by `public/step_worker.js` only after a STEP source is converted. ## What this evidence does and does not decide Decided: - The worker split is the correct architecture boundary for client-side STEP: CAD conversion stays in a separate STEP worker, and rendering remains in the renderer worker. - The published full npm `opencascade.js@1.1.1` artifact is not a viable Polymodel STEP→GLB path: it is large and lacks required bindings for the target XDE/GLB workflow. - A single-threaded custom OpenCascade.js build can convert STEP fixtures to GLB in a browser Worker without cross-origin isolation. - The existing renderer can load converted GLB output through the current `blob:` object-URL handoff. Not decided: - A multithreaded pthread custom build was not measured. - Whether the product path should keep the `blob:` handoff or add direct transferred GLB bytes to the renderer protocol. Direct bytes would avoid object URL lifetime/fetch semantics, but the current handoff works.