Problem
packages/engine/src/dfm.ts::runDfm evaluates the document and walks every part calling Solid.runDfm on the main thread. With the BVH-raymarched samplers (added on the claude/dfm-tools-integration-HcsqQ branch), thickness + accessibility + overhang each cost roughly O(F log F) per face, so a ~12-part assembly with ~50 faces per part can spend 100+ ms in DFM during parameter scrubbing — enough to drop frames in the viewport.
The eval worker already exists at packages/engine/src/eval-worker.ts but only handles type === "evaluate" (line 178). DFM has no worker path, so the live-check loop in packages/app/src/components/DfmPanel.tsx blocks the main thread on every debounced re-run.
Why now
Live DFM during scrubbing is the user-facing differentiator vs. competitors' button-press DFM. If we ship it but it stutters, the experience regresses. Worker-hosted DFM is the right way to keep the headline UX promise.
Proposed approach
- Extend the worker protocol with a
type === "dfm" message:
{ type: "dfm", id, docJson, process, rulePackToml }
→ { type: "dfm-result", id, reportJson }
- Inside the worker, evaluate the doc (it already has the kernel handle for
evaluate), then call Solid.runDfm per part — same logic that's currently in runDfm on the main thread.
- Update
runDfm in packages/engine/src/dfm.ts to post to the worker via the existing evaluateInWorker plumbing pattern in Engine (packages/engine/src/index.ts:497), then resolve with the parsed DfmReport.
- Add a worker pool or per-doc cache so concurrent runs (e.g., user toggles process while previous run is in flight) don't pile up.
Acceptance criteria
- DFM runs no longer block the main thread; scrubbing stays at 60fps with live DFM enabled on a 50-face part.
- The MCP
dfm_check tool keeps working (it can either still call the synchronous in-process path or use the worker — either is fine).
- A perf test in
packages/engine/__tests__/ confirms runDfm returns within ~50ms for a single primitive and doesn't block when run repeatedly.
References
packages/engine/src/eval-worker.ts:178 — current case "evaluate" switch where the new dfm message slots in
packages/engine/src/index.ts:497 — evaluateInWorker plumbing pattern to mirror
packages/engine/src/dfm.ts — current main-thread runDfm implementation
packages/app/src/stores/dfm-store.ts — debounced re-run loop that benefits
Problem
packages/engine/src/dfm.ts::runDfmevaluates the document and walks every part callingSolid.runDfmon the main thread. With the BVH-raymarched samplers (added on theclaude/dfm-tools-integration-HcsqQbranch), thickness + accessibility + overhang each cost roughly O(F log F) per face, so a ~12-part assembly with ~50 faces per part can spend 100+ ms in DFM during parameter scrubbing — enough to drop frames in the viewport.The eval worker already exists at
packages/engine/src/eval-worker.tsbut only handlestype === "evaluate"(line 178). DFM has no worker path, so the live-check loop inpackages/app/src/components/DfmPanel.tsxblocks the main thread on every debounced re-run.Why now
Live DFM during scrubbing is the user-facing differentiator vs. competitors' button-press DFM. If we ship it but it stutters, the experience regresses. Worker-hosted DFM is the right way to keep the headline UX promise.
Proposed approach
type === "dfm"message:evaluate), then callSolid.runDfmper part — same logic that's currently inrunDfmon the main thread.runDfminpackages/engine/src/dfm.tsto post to the worker via the existingevaluateInWorkerplumbing pattern inEngine(packages/engine/src/index.ts:497), then resolve with the parsedDfmReport.Acceptance criteria
dfm_checktool keeps working (it can either still call the synchronous in-process path or use the worker — either is fine).packages/engine/__tests__/confirmsrunDfmreturns within ~50ms for a single primitive and doesn't block when run repeatedly.References
packages/engine/src/eval-worker.ts:178— currentcase "evaluate"switch where the newdfmmessage slots inpackages/engine/src/index.ts:497—evaluateInWorkerplumbing pattern to mirrorpackages/engine/src/dfm.ts— current main-threadrunDfmimplementationpackages/app/src/stores/dfm-store.ts— debounced re-run loop that benefits