From 97d4f9657e9ad21182cbbe77ab8b2608715ea7d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20T=C3=B6rcsv=C3=A1ri?= Date: Thu, 27 Aug 2026 15:26:30 +0200 Subject: [PATCH] read-only-viewer 0003: no lib catalog for viewers; 3D viewer from the session menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - kicadShow3DViewer embind (pcbnew-only name, registered in the unguarded section so the merged kicad_editor image carries it): runOnCoroutine → ACTIONS::show3DViewer. - Session menu: "3D viewer" row (pcbnew) — the only 3D entry once the wx chrome is hidden. Runs the deferred model prescan first. - Read-only sessions: skip the boot-time enableRealtime scope-room socket (would 401 for non-members) and defer the board's 3D-model prescan until the viewer is opened (deferBoardModelPrescan / runDeferredModelPrescan). - kicad submodule → read-only allowlist for the 3D viewer actions. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PXCntRWNn89M72EkrqMkvc --- kicad | 2 +- wasm/bindings/pcbnew_embind.cpp | 20 ++++++++ web/standalone/src/components/WasmTool.tsx | 26 +++++++++- .../src/components/wasm-tool/SessionMenu.tsx | 16 +++++++ .../src/components/wasm-tool/ui-helpers.ts | 9 ++++ web/standalone/src/wasm/kicad-runner.ts | 13 +++-- .../src/wasm/libs/models-bridge.test.ts | 47 +++++++++++++++++++ web/standalone/src/wasm/libs/models-bridge.ts | 25 ++++++++++ 8 files changed, 152 insertions(+), 6 deletions(-) diff --git a/kicad b/kicad index 27051b4..39da876 160000 --- a/kicad +++ b/kicad @@ -1 +1 @@ -Subproject commit 27051b46e2b3662731f913355a2bd752ca5c6561 +Subproject commit 39da876c2df6326392b240453a485f28f91231f8 diff --git a/wasm/bindings/pcbnew_embind.cpp b/wasm/bindings/pcbnew_embind.cpp index a3d612d..d974bab 100644 --- a/wasm/bindings/pcbnew_embind.cpp +++ b/wasm/bindings/pcbnew_embind.cpp @@ -1898,6 +1898,23 @@ void pcbCollabFitViewport( double aCx, double aCy, double aHalfW, double aHalfH presenceCore().fitViewport( aCx, aCy, aHalfW, aHalfH ); } +// JS → C++: open the board's 3D viewer (read-only-viewer / hide-UI sessions have +// no wx View menu). Dispatched like a native edit — CallAfter + COROUTINE via +// runOnCoroutine — so the tool runs on its own stack, where the 3D frame's +// RunMainStack bounce (the 8/12 sleep-park fix) is valid; never from inside a +// parked bridge crossing. Returns false when no board frame is up. +bool pcbShow3DViewer() +{ + PCB_EDIT_FRAME* fr = pcbFrame(); + + if( !fr ) + return false; + + pcbjam_collab::runOnCoroutine( + fr, [fr]() { fr->GetToolManager()->RunAction( ACTIONS::show3DViewer ); } ); + return true; +} + // JS pull of the current viewport transform (world↔screen mapping for the DOM layer): // `{cx,cy,scale,w,h}` — world center, pixels-per-IU scale, canvas size in px. std::string pcbCollabGetViewport() @@ -2795,6 +2812,9 @@ EMSCRIPTEN_BINDINGS(pcbnew) { function("kicadLayersGetState", &pcbLayersGetState); function("kicadLayersSetVisible", &pcbLayersSetVisible); function("kicadLayersSetActive", &pcbLayersSetActive); + // Session-menu 3D entry (read-only-viewer 0003) — pcbnew-only name, + // merged-image safe (false when no board frame is up). + function("kicadShow3DViewer", &pcbShow3DViewer); // pcbnew-only test helper (no eeschema counterpart — name is not shared). function("kicadCollabTestItemBlob", &kicadCollabTestItemBlob); // pcbnew-only ysync-review repro hooks (names not shared with eeschema). diff --git a/web/standalone/src/components/WasmTool.tsx b/web/standalone/src/components/WasmTool.tsx index 5299c4f..1a1fa2d 100644 --- a/web/standalone/src/components/WasmTool.tsx +++ b/web/standalone/src/components/WasmTool.tsx @@ -118,9 +118,11 @@ import { waitForWxUi, } from "@/components/wasm-tool/collab-start"; import { installQuitHook } from "@/components/wasm-tool/quit-hook"; +import { runDeferredModelPrescan } from "@/wasm/libs/models-bridge"; import { installToolNavigationHook } from "@/components/wasm-tool/tool-navigation"; import { chromeSetter, + show3DOpener, COLLAB_TOOLS, INSPECTOR_OPEN_KEY, LAYERS_OPEN_KEY, @@ -1065,6 +1067,8 @@ export function WasmTool({ slug, files, targetPath, + // Viewers fetch 3D bodies only if they open the viewer (session menu). + deferModelPrescan: readOnly, // ydoc source with a populated room: the target file's bytes come // from the doc; everything else (sibling files) still fetches. fetchBytes: @@ -1386,7 +1390,10 @@ export function WasmTool({ // DOCUMENT references — a peer editing a PLACED symbol must still // reach this session live (lib-update toast); everything else syncs // on the next load. Fire-and-forget: boot never waits on sockets. - if (targetPath && source?.enableRealtime) { + // Read-only sessions carry no editable libs (the boot payload is + // model3d-only for them) and may not open the team's scope room — + // skip the socket rather than collect a 401. + if (targetPath && source?.enableRealtime && !readOnly) { const staged = readStagedFile(win, slug, targetPath); const nicks = staged ? usedLibNicknames(new TextDecoder().decode(staged)) @@ -1472,6 +1479,22 @@ export function WasmTool({ [ready], ); + // kicadShow3DViewer — the session-menu 3D entry (read-only-viewer / hide-UI + // have no wx View menu). Runs the deferred model prescan first so a viewer's + // first open resolves its refs from MEMFS instead of one C++ ensure each. + const show3DFn = React.useMemo(() => { + if (!ready || tool !== "pcbnew") return null; + const open = show3DOpener(window); + if (!open) return null; + return () => { + void runDeferredModelPrescan() + .catch((e) => append(`[3d] deferred prescan failed: ${String(e)}`)) + .then(() => { + if (!open()) append("[3d] kicadShow3DViewer: no board frame"); + }); + }; + }, [ready, tool, append]); + // Layer bridge (viewer-panels), pcbnew sessions only — the merged bundle // exports the names for every frame, but they no-op on a non-PCB frame. const layersMod = React.useMemo(() => { @@ -1604,6 +1627,7 @@ export function WasmTool({ canToggleChrome={setChromeFn !== null} chromeHidden={chromeHidden} onToggleChrome={() => toggleChromeHidden()} + onShow3D={show3DFn} /> )} diff --git a/web/standalone/src/components/wasm-tool/SessionMenu.tsx b/web/standalone/src/components/wasm-tool/SessionMenu.tsx index e87d94a..41d1e6f 100644 --- a/web/standalone/src/components/wasm-tool/SessionMenu.tsx +++ b/web/standalone/src/components/wasm-tool/SessionMenu.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import { AlertTriangle, + Box, Crosshair, EyeOff, Layers, @@ -155,6 +156,7 @@ export function SessionMenu({ canToggleChrome, chromeHidden, onToggleChrome, + onShow3D, }: { tool: Tool; readOnly: boolean; @@ -184,6 +186,9 @@ export function SessionMenu({ canToggleChrome: boolean; chromeHidden: boolean; onToggleChrome: () => void; + /** Open the board's 3D viewer (pcbnew only); null when the bundle lacks + * the bridge. The only 3D entry point once the wx menus are hidden. */ + onShow3D: (() => void) | null; }) { return ( {inspectorOpen ? "Hide inspector" : "Inspector"} )} + {tool === "pcbnew" && onShow3D && ( + + )} {canToggleChrome && !readOnly && (