fix(web): boot KiCad WASM in-document (no iframe) and fix eeschema frame sizing
Replace the same-origin iframe in WasmTool with a direct in-document boot (src/wasm/boot.ts): build the global Emscripten Module + preRun steps and inject wx.js + <tool>.js into the page, the same artifacts the e2e harness uses. The build is non-modularized (global Module/FS) and pthread-based, so locateFile/mainScriptUrlOrBlob are set so the wasm + worker load regardless of the SPA route, and only one tool runs per page load. Two bugs found during in-browser verification: - This build does not export Module.FS (touching it aborts); use the global window.FS like the harness does. - The wasm reads top-level frame geometry from a global `mainWindow` (offsetWidth/offsetHeight/offsetTop), falling back to a hardcoded 1280x720 when undefined. The harness sets it via `var mainWindow = ...`; we must too, or the frame mismatches the viewport and the whole AUI layout breaks (missing toolbars, transparent/ghosted panels). Expose the #main-window element as window.mainWindow. Verified: eeschema renders the full UI (menus, toolbars, panels, schematic) matching the e2e baseline. pcbnew remains pre-existing-broken at the build level (raw pcbnew.html harness is equally broken: empty registry, dynCall "ii signature" errors), independent of this change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
18a9de0449
commit
83462f7090
8 changed files with 311 additions and 75 deletions
|
|
@ -66,10 +66,15 @@ them at `/wasm` (same origin). `VITE_WASM_ASSET_BASE_URL` defaults to `/wasm`.
|
|||
- If the tool won't load, the target dir is probably empty — run
|
||||
`tests/scripts/setup-kicad-wasm.sh` to populate `tests/apps/kicad/`.
|
||||
|
||||
The tool view (`WasmTool.tsx`) loads the **actual harness** (`/wasm/<tool>.html`,
|
||||
the same page the e2e tests use) in a same-origin iframe, then injects the
|
||||
project tree into its MEMFS and drives File→Open — reusing the proven loader
|
||||
rather than re-implementing the Emscripten bootstrap.
|
||||
The tool view (`WasmTool.tsx` + `src/wasm/boot.ts`) boots the tool **directly in
|
||||
the React document** — no iframe. It replicates the proven harness HTML
|
||||
(`tests/apps/kicad/<tool>.html`): builds the same global Emscripten `Module`
|
||||
config and preRun steps (create canvas, write `images.tar.gz`, seed config), then
|
||||
injects the same `wx.js` + `<tool>.js` artifacts into the page. It then syncs the
|
||||
project tree into MEMFS and drives File→Open. The build is non-modularized
|
||||
(global `Module`/`FS`) and pthread-based, so only **one** tool runs per page load;
|
||||
switching tools requires a full navigation. `locateFile` resolves the wasm and
|
||||
the pthread worker against `<base>` so they load regardless of the SPA route.
|
||||
|
||||
**prod**: point `VITE_WASM_ASSET_BASE_URL` at a CDN URL — but that origin must
|
||||
itself satisfy the same-origin / COEP constraints (e.g. served under the app's
|
||||
|
|
|
|||
|
|
@ -3,13 +3,15 @@ import type { ProjectFile, Tool } from "@kicad-web/contract";
|
|||
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { fetchFileBytes } from "@/lib/api";
|
||||
import { WASM_ASSET_BASE_URL } from "@/lib/config";
|
||||
import { driveProjectIntoTool, hookIframeConsole } from "@/wasm/kicad-runner";
|
||||
import { bootKicadTool } from "@/wasm/boot";
|
||||
import { driveProjectIntoTool } from "@/wasm/kicad-runner";
|
||||
|
||||
/**
|
||||
* Boots a KiCad tool by loading the proven harness HTML (/wasm/<tool>.html, the
|
||||
* same file the e2e tests use) in a same-origin iframe, then injects the project
|
||||
* tree into its MEMFS and drives File→Open. Same-origin is required: KiCad WASM
|
||||
* refuses to load its glue/wasm from a different origin under COEP.
|
||||
* Boots a KiCad tool directly in this React document (no iframe): builds the
|
||||
* Emscripten `Module` config, injects the proven harness scripts (wx.js +
|
||||
* <tool>.js, the same artifacts the e2e tests use) into the page, then syncs the
|
||||
* project tree into MEMFS and drives File→Open. See src/wasm/boot.ts for why the
|
||||
* runtime is single-instance per page load.
|
||||
*/
|
||||
export function WasmTool({
|
||||
tool,
|
||||
|
|
@ -22,54 +24,62 @@ export function WasmTool({
|
|||
files: ProjectFile[];
|
||||
targetPath?: string;
|
||||
}) {
|
||||
const iframeRef = React.useRef<HTMLIFrameElement>(null);
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const startedRef = React.useRef(false);
|
||||
const [status, setStatus] = React.useState("Loading tool…");
|
||||
const [logs, setLogs] = React.useState<string[]>([]);
|
||||
const [showLog, setShowLog] = React.useState(false);
|
||||
|
||||
const base = WASM_ASSET_BASE_URL.replace(/\/$/, "");
|
||||
const src = `${base}/${tool}.html`;
|
||||
|
||||
const onLoad = () => {
|
||||
React.useEffect(() => {
|
||||
// Guard re-entry: the WASM runtime is process-global and must boot exactly
|
||||
// once (see boot.ts). StrictMode is disabled app-wide for the same reason.
|
||||
if (startedRef.current) return;
|
||||
startedRef.current = true;
|
||||
const win = iframeRef.current?.contentWindow as
|
||||
| ToolWindow
|
||||
| null
|
||||
| undefined;
|
||||
if (!win) {
|
||||
setStatus("Error: iframe has no window");
|
||||
|
||||
const container = containerRef.current;
|
||||
if (!container) {
|
||||
setStatus("Error: tool container not mounted");
|
||||
return;
|
||||
}
|
||||
|
||||
const append = (msg: string) =>
|
||||
setLogs((prev) => [...prev.slice(-800), msg]);
|
||||
hookIframeConsole(win, append);
|
||||
const win = window as ToolWindow;
|
||||
|
||||
void driveProjectIntoTool(win, {
|
||||
tool,
|
||||
slug,
|
||||
files,
|
||||
targetPath,
|
||||
fetchBytes: (relPath) => fetchFileBytes(slug, relPath),
|
||||
log: append,
|
||||
onStatus: setStatus,
|
||||
}).catch((err) => {
|
||||
append(`[fatal] ${String(err)}`);
|
||||
setStatus(`Error: ${String(err)}`);
|
||||
});
|
||||
};
|
||||
void (async () => {
|
||||
try {
|
||||
await bootKicadTool({ tool, base, container, log: append, onStatus: setStatus });
|
||||
await driveProjectIntoTool(win, {
|
||||
tool,
|
||||
slug,
|
||||
files,
|
||||
targetPath,
|
||||
fetchBytes: (relPath) => fetchFileBytes(slug, relPath),
|
||||
log: append,
|
||||
onStatus: setStatus,
|
||||
});
|
||||
} catch (err) {
|
||||
append(`[fatal] ${String(err)}`);
|
||||
setStatus(`Error: ${String(err)}`);
|
||||
}
|
||||
})();
|
||||
// Boot is one-shot per mount; deps intentionally exclude files/targetPath so
|
||||
// they don't retrigger a (rejected) second boot.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tool, slug, base]);
|
||||
|
||||
return (
|
||||
<div className="relative h-screen w-screen overflow-hidden bg-[#1a1a2e]">
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={src}
|
||||
title={`${tool} (${slug})`}
|
||||
onLoad={onLoad}
|
||||
className="absolute inset-0 h-full w-full border-0"
|
||||
allow="cross-origin-isolated; fullscreen"
|
||||
/>
|
||||
{/*
|
||||
wx.js addresses the DOM by id: #main-window is its top-level (id=0)
|
||||
window — it owns #canvas (created in boot's preRun) — and #window-container
|
||||
parents every child window. Both ids must exist before the runtime boots,
|
||||
mirroring the harness HTML (tests/apps/kicad/<tool>.html).
|
||||
*/}
|
||||
<div ref={containerRef} id="main-window" className="absolute inset-0 h-full w-full" />
|
||||
<div id="window-container" />
|
||||
|
||||
{status && (
|
||||
<div className="pointer-events-none absolute left-3 top-3 z-20 rounded bg-black/70 px-3 py-2 font-mono text-xs text-white">
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@ const queryClient = new QueryClient({
|
|||
});
|
||||
|
||||
// NOTE: deliberately NOT wrapped in <React.StrictMode>. StrictMode double-mounts
|
||||
// components in dev, which tears down and recreates the WasmTool iframe and thus
|
||||
// instantiates a 175–338 MB KiCad wasm twice — enough to OOM the tab. The tool
|
||||
// view must instantiate exactly once per navigation.
|
||||
// components in dev, which would re-run WasmTool's boot effect and try to
|
||||
// instantiate a 175–338 MB KiCad wasm twice — enough to OOM the tab (and the
|
||||
// runtime is process-global anyway; see src/wasm/boot.ts). The tool view must
|
||||
// instantiate exactly once per navigation.
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
|
|
|
|||
219
web/apps/frontend/src/wasm/boot.ts
Normal file
219
web/apps/frontend/src/wasm/boot.ts
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
import type { Tool } from "@kicad-web/contract";
|
||||
import {
|
||||
KICAD_CONFIG_DIR,
|
||||
RESOURCE_PATH,
|
||||
TOOL_ARGV0,
|
||||
TOOL_NEEDS_CONFIG_SEED,
|
||||
} from "./constants";
|
||||
|
||||
/**
|
||||
* Boot a KiCad tool directly in the main React document — no iframe.
|
||||
*
|
||||
* This is a faithful port of the proven harness HTML (tests/apps/kicad/<tool>.html):
|
||||
* it builds the same global `Module` config, runs the same preRun steps (create
|
||||
* canvas, write images.tar.gz, seed config), then injects `wx.js` followed by
|
||||
* `<tool>.js`. The KiCad WASM build is NON-modularized, so it reads a global
|
||||
* `var Module` and publishes `FS`/`wxElementRegistry` onto `window` — exactly the
|
||||
* surface the iframe approach used, only now in the top-level window.
|
||||
*
|
||||
* Two browser facts make running in the main document (rather than at /wasm/...)
|
||||
* work without touching the build:
|
||||
* - `locateFile` is overridden to resolve `<base>/<file>`, so the .wasm and the
|
||||
* pthread worker script are fetched from the asset dir regardless of the
|
||||
* SPA route the user is on.
|
||||
* - `mainScriptUrlOrBlob` pins the pthread worker to `<base>/<tool>.js`
|
||||
* (same-origin — required: KiCad's pthreads cannot spawn cross-origin).
|
||||
*
|
||||
* Single-instance: the build owns process-global state (one `Module`, one wasm
|
||||
* memory) so only ONE tool can run per page load. A second boot — switching
|
||||
* tools, or a stray double-mount — is rejected; switching tools requires a full
|
||||
* page navigation (which gives a fresh global scope, same as loading a new HTML).
|
||||
*/
|
||||
export interface BootOptions {
|
||||
tool: Tool;
|
||||
/** Asset base (no trailing slash) where wx.js / <tool>.{js,wasm} / images.tar.gz live. */
|
||||
base: string;
|
||||
/** Full-screen element that will host the Emscripten <canvas>. */
|
||||
container: HTMLElement;
|
||||
log: (msg: string) => void;
|
||||
onStatus: (text: string) => void;
|
||||
}
|
||||
|
||||
let booted: { tool: Tool; promise: Promise<void> } | null = null;
|
||||
|
||||
/**
|
||||
* Inject and start the tool's WASM into `window`. Resolves once the glue scripts
|
||||
* are loaded (runtime init continues asynchronously afterwards — callers that
|
||||
* need the filesystem should wait on `window.FS`, as driveProjectIntoTool does).
|
||||
*/
|
||||
export function bootKicadTool(opts: BootOptions): Promise<void> {
|
||||
if (booted) {
|
||||
if (booted.tool === opts.tool) return booted.promise;
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
`KiCad "${booted.tool}" is already running in this page; its WASM runtime ` +
|
||||
`is process-global and cannot be torn down. Reload the page to open ` +
|
||||
`"${opts.tool}".`,
|
||||
),
|
||||
);
|
||||
}
|
||||
const promise = doBoot(opts);
|
||||
booted = { tool: opts.tool, promise };
|
||||
return promise;
|
||||
}
|
||||
|
||||
function loadScript(src: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const s = document.createElement("script");
|
||||
s.src = src;
|
||||
// currentScript.src (absolute) is what Emscripten captures as `_scriptName`
|
||||
// and uses to derive the script dir + the pthread worker URL.
|
||||
s.onload = () => resolve();
|
||||
s.onerror = () => reject(new Error(`failed to load script: ${src}`));
|
||||
document.body.appendChild(s);
|
||||
});
|
||||
}
|
||||
|
||||
async function doBoot(opts: BootOptions): Promise<void> {
|
||||
const { tool, base, container, log, onStatus } = opts;
|
||||
const w = window as ToolWindow;
|
||||
|
||||
// The wasm reads the top-level frame geometry from a GLOBAL `mainWindow`
|
||||
// (mainWindow.offsetWidth/offsetHeight/offsetTop — see <tool>.js). The harness
|
||||
// HTML defines it as `var mainWindow = document.getElementById('main-window')`;
|
||||
// we must do the same or the wasm falls back to a hardcoded 1280x720 frame that
|
||||
// mismatches the viewport, breaking the whole AUI layout (toolbars/panels).
|
||||
(w as unknown as { mainWindow: HTMLElement }).mainWindow = container;
|
||||
|
||||
onStatus("Downloading…");
|
||||
|
||||
// Prefetch images.tar.gz in parallel with the (much larger) wasm download —
|
||||
// exactly as the harness does. writeResources (in preRun) writes it once ready.
|
||||
let resourceData: Uint8Array | null = null;
|
||||
void fetch(`${base}/images.tar.gz`)
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
return r.arrayBuffer();
|
||||
})
|
||||
.then((buf) => {
|
||||
resourceData = new Uint8Array(buf);
|
||||
log(`[boot] prefetched images.tar.gz (${resourceData.length} bytes)`);
|
||||
})
|
||||
.catch((err) => log(`[boot] images.tar.gz prefetch failed: ${String(err)}`));
|
||||
|
||||
// Use the GLOBAL `FS` (window.FS), exactly as the harness HTML does. This build
|
||||
// does NOT export `Module.FS` (touching it aborts: "'FS' was not exported"), but
|
||||
// the non-modularized glue declares `var FS` at global scope, so window.FS is
|
||||
// live from script-eval time — before preRun runs.
|
||||
const moduleFS = (): EmscriptenFS => {
|
||||
const FS = w.FS;
|
||||
if (!FS) throw new Error("global FS not available in preRun");
|
||||
return FS;
|
||||
};
|
||||
|
||||
// preRun: create the canvas the tool renders into and mount it in our container.
|
||||
const createCanvas = () => {
|
||||
const canvas = w.document.createElement("canvas");
|
||||
canvas.id = "canvas";
|
||||
canvas.style.display = "none";
|
||||
// wx.js owns the backing-store size via setWindowRect(); we set only CSS size.
|
||||
const width = w.innerWidth;
|
||||
const height = w.innerHeight;
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
canvas.oncontextmenu = (e) => e.preventDefault();
|
||||
canvas.addEventListener(
|
||||
"webglcontextlost",
|
||||
(e) => {
|
||||
onStatus("WebGL context lost — reload the page.");
|
||||
e.preventDefault();
|
||||
},
|
||||
false,
|
||||
);
|
||||
container.appendChild(canvas);
|
||||
(w.Module as { canvas: HTMLCanvasElement }).canvas = canvas;
|
||||
log(`[boot] canvas created ${width}x${height}`);
|
||||
};
|
||||
|
||||
// preRun: write the compiled-in KICAD_DATA resources (icons, etc.).
|
||||
const writeResources = () => {
|
||||
const FS = moduleFS();
|
||||
FS.mkdirTree(RESOURCE_PATH);
|
||||
if (resourceData) {
|
||||
FS.writeFile(`${RESOURCE_PATH}/images.tar.gz`, resourceData);
|
||||
log(`[boot] wrote images.tar.gz to ${RESOURCE_PATH}`);
|
||||
} else {
|
||||
log("[boot] images.tar.gz not ready at preRun (wasm beat the fetch)");
|
||||
}
|
||||
};
|
||||
|
||||
// preRun (seeding tools only): suppress the first-run setup wizard, whose modal
|
||||
// loop crashes Asyncify in our ephemeral MEMFS. Make all settings providers
|
||||
// report NeedsUserInput()==false — the wizard's "use defaults" path.
|
||||
const seedKicadConfig = () => {
|
||||
const FS = moduleFS();
|
||||
FS.mkdirTree(KICAD_CONFIG_DIR);
|
||||
const writeIfAbsent = (path: string, contents: string) => {
|
||||
if (FS.analyzePath(path).exists) return;
|
||||
FS.writeFile(path, contents);
|
||||
log(`[boot] seeded ${path}`);
|
||||
};
|
||||
writeIfAbsent(
|
||||
`${KICAD_CONFIG_DIR}/kicad_common.json`,
|
||||
JSON.stringify(
|
||||
{
|
||||
do_not_show_again: {
|
||||
update_check_prompt: true,
|
||||
data_collection_prompt: true,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
writeIfAbsent(
|
||||
`${KICAD_CONFIG_DIR}/sym-lib-table`,
|
||||
"(sym_lib_table\n (version 7)\n)\n",
|
||||
);
|
||||
writeIfAbsent(
|
||||
`${KICAD_CONFIG_DIR}/fp-lib-table`,
|
||||
"(fp_lib_table\n (version 7)\n)\n",
|
||||
);
|
||||
writeIfAbsent(
|
||||
`${KICAD_CONFIG_DIR}/design-block-lib-table`,
|
||||
"(design_block_lib_table\n (version 7)\n)\n",
|
||||
);
|
||||
};
|
||||
|
||||
const preRun = [createCanvas, writeResources];
|
||||
if (TOOL_NEEDS_CONFIG_SEED[tool]) preRun.push(seedKicadConfig);
|
||||
|
||||
w.Module = {
|
||||
thisProgram: TOOL_ARGV0[tool], // argv[0] for KiCad's DEBUG check
|
||||
preRun,
|
||||
postRun: [],
|
||||
print: (...args: unknown[]) => log(`[out] ${args.join(" ")}`),
|
||||
printErr: (...args: unknown[]) => log(`[err] ${args.join(" ")}`),
|
||||
setStatus: (text: string) => {
|
||||
if (text) onStatus(text);
|
||||
},
|
||||
monitorRunDependencies: () => {},
|
||||
onRuntimeInitialized: () => {
|
||||
log("[boot] runtime initialized");
|
||||
const canvas = (w.Module as { canvas?: HTMLCanvasElement }).canvas;
|
||||
if (canvas) canvas.style.display = "block";
|
||||
onStatus("");
|
||||
},
|
||||
// Resolve wasm + pthread worker against the asset base, not the SPA route.
|
||||
locateFile: (path: string) => `${base}/${path}`,
|
||||
// Pin the pthread worker script (must be same-origin).
|
||||
mainScriptUrlOrBlob: `${base}/${tool}.js`,
|
||||
};
|
||||
|
||||
// wx.js MUST load first: it defines globals the wasm imports (getConfigEntryLength,
|
||||
// …) and the wxElementRegistry the open-flow drives. Then the tool glue, whose
|
||||
// execution captures currentScript.src as Emscripten's _scriptName.
|
||||
await loadScript(`${base}/wx.js`);
|
||||
await loadScript(`${base}/${tool}.js`);
|
||||
log(`[boot] injected wx.js + ${tool}.js (base=${base})`);
|
||||
}
|
||||
|
|
@ -15,13 +15,32 @@ export const MEMFS_PROJECTS_DIR = `/home/kicad/documents/kicad/${KICAD_VERSION_D
|
|||
export const RESOURCE_PATH =
|
||||
"/workspace/build-wasm/sysroot/share/kicad/resources";
|
||||
|
||||
/** argv[0] each tool's DEBUG check expects (see tests/apps/kicad/pcbnew.html). */
|
||||
/**
|
||||
* argv[0] each tool's DEBUG check expects. These MUST match the values the
|
||||
* proven harness HTMLs set as `Module.thisProgram` (tests/apps/kicad/<tool>.html)
|
||||
* — notably the calculator binary is `pcb_calculator`, not `calculator`.
|
||||
*/
|
||||
export const TOOL_ARGV0: Record<Tool, string> = {
|
||||
pcbnew: "/usr/bin/pcbnew",
|
||||
eeschema: "/usr/bin/eeschema",
|
||||
calculator: "/usr/bin/calculator",
|
||||
calculator: "/usr/bin/pcb_calculator",
|
||||
};
|
||||
|
||||
/**
|
||||
* Tools whose standalone entry (single_top.cpp) runs the first-run setup wizard
|
||||
* on launch. That wizard's modal loop crashes Asyncify in our ephemeral MEMFS,
|
||||
* so for these we seed a default KiCad config before main() to suppress it.
|
||||
* Mirrors which harness HTMLs include `seedKicadConfig` in preRun (eeschema only).
|
||||
*/
|
||||
export const TOOL_NEEDS_CONFIG_SEED: Record<Tool, boolean> = {
|
||||
pcbnew: false,
|
||||
eeschema: true,
|
||||
calculator: false,
|
||||
};
|
||||
|
||||
/** KiCad user settings dir for this build (PATHS::GetUserSettingsPath()). */
|
||||
export const KICAD_CONFIG_DIR = `/home/kicad/.config/kicad/kicad/${KICAD_VERSION_DIR}`;
|
||||
|
||||
export function memfsProjectDir(slug: string): string {
|
||||
return `${MEMFS_PROJECTS_DIR}/${slug}`;
|
||||
}
|
||||
|
|
|
|||
7
web/apps/frontend/src/wasm/global.d.ts
vendored
7
web/apps/frontend/src/wasm/global.d.ts
vendored
|
|
@ -46,8 +46,9 @@ declare global {
|
|||
wxElementRegistry?: WxElementRegistry;
|
||||
}
|
||||
|
||||
// A real browsing-context window (iframe.contentWindow): the Window interface
|
||||
// PLUS the global declarations (console, PointerEvent, document, …) that live
|
||||
// on `typeof globalThis`, not on the bare Window interface.
|
||||
// The browsing-context window the tool runs in — now the top-level `window`
|
||||
// (the WASM boots in-document, not in an iframe). The Window interface PLUS the
|
||||
// global declarations (console, PointerEvent, document, …) that live on
|
||||
// `typeof globalThis`, not on the bare Window interface.
|
||||
type ToolWindow = Window & typeof globalThis;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,32 +29,13 @@ async function waitFor<T>(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward the iframe's console (where the harness routes Module.print/printErr
|
||||
* and KiCad logs) into our on-page log panel, preserving the original output.
|
||||
*/
|
||||
export function hookIframeConsole(win: ToolWindow, log: (msg: string) => void): void {
|
||||
const wrap = (level: "log" | "info" | "warn" | "error") => {
|
||||
const orig = win.console[level].bind(win.console);
|
||||
win.console[level] = (...args: unknown[]) => {
|
||||
try {
|
||||
log(args.map((a) => (typeof a === "string" ? a : String(a))).join(" "));
|
||||
} catch {
|
||||
/* ignore logging errors */
|
||||
}
|
||||
orig(...args);
|
||||
};
|
||||
};
|
||||
(["log", "info", "warn", "error"] as const).forEach(wrap);
|
||||
}
|
||||
|
||||
function getFS(win: ToolWindow): EmscriptenFS {
|
||||
const fs = win.FS ?? win.Module?.FS;
|
||||
if (!fs) throw new Error("Emscripten FS not available in iframe");
|
||||
if (!fs) throw new Error("Emscripten FS not available");
|
||||
return fs as EmscriptenFS;
|
||||
}
|
||||
|
||||
/** Mirror the whole project tree into the iframe's MEMFS (sync-whole-tree). */
|
||||
/** Mirror the whole project tree into the tool's MEMFS (sync-whole-tree). */
|
||||
async function syncProjectToMemfs(win: ToolWindow, opts: DriveOptions): Promise<void> {
|
||||
const fs = getFS(win);
|
||||
fs.mkdirTree(memfsProjectDir(opts.slug));
|
||||
|
|
@ -69,9 +50,9 @@ async function syncProjectToMemfs(win: ToolWindow, opts: DriveOptions): Promise<
|
|||
}
|
||||
|
||||
/**
|
||||
* Drive a project into an already-booting tool harness (loaded in a same-origin
|
||||
* iframe at /wasm/<tool>.html). Waits for the Emscripten FS, syncs the project
|
||||
* tree into MEMFS, then auto-opens the target file.
|
||||
* Drive a project into an already-booting tool runtime (booted into `win` by
|
||||
* bootKicadTool — the top-level window). Waits for the Emscripten FS, syncs the
|
||||
* project tree into MEMFS, then auto-opens the target file.
|
||||
*/
|
||||
export async function driveProjectIntoTool(
|
||||
win: ToolWindow,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* Drive the tool (running in a same-origin iframe `win`) to open a file already
|
||||
* written into its MEMFS.
|
||||
* Drive the tool (running in `win` — the top-level window) to open a file
|
||||
* already written into its MEMFS.
|
||||
*
|
||||
* Two strategies, tried in order:
|
||||
* 1. Programmatic hook — `win.Module.kicadOpenFile(path)` if the build exposes
|
||||
|
|
@ -48,7 +48,7 @@ function canvasOf(win: ToolWindow): HTMLCanvasElement | null {
|
|||
return (win.Module?.canvas as HTMLCanvasElement) ?? null;
|
||||
}
|
||||
|
||||
/** Dispatch a full pointer+mouse click at page coordinates on the iframe canvas. */
|
||||
/** Dispatch a full pointer+mouse click at page coordinates on the tool canvas. */
|
||||
function clickAt(win: ToolWindow, x: number, y: number): void {
|
||||
const el = canvasOf(win);
|
||||
if (!el) return;
|
||||
|
|
|
|||
Loading…
Reference in a new issue