# 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.
