pcb-schema switch
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f936eb17b0
commit
ee451c9de2
6 changed files with 303 additions and 56 deletions
2
kicad
2
kicad
|
|
@ -1 +1 @@
|
||||||
Subproject commit ac7d733787000f7988a122559888bc8d76924315
|
Subproject commit 8192a71cc2c21f76c41c822a3f0db9998b28ca86
|
||||||
|
|
@ -1,21 +1,20 @@
|
||||||
import { defineConfig, devices } from '@playwright/test';
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* E2E config for the React WEB APP (apps/frontend at :3048 + apps/server :3050),
|
* E2E config for the React WEB APP (web/standalone editor at :3048 + the
|
||||||
* as opposed to playwright-kicad.config.ts which drives the standalone tool
|
* @pcbjam/backend-example reference backend at :3060), as opposed to
|
||||||
* harness HTMLs under tests/apps/kicad.
|
* playwright-kicad.config.ts which drives the standalone tool harness HTMLs
|
||||||
|
* under tests/apps/kicad.
|
||||||
*
|
*
|
||||||
* These tests exercise the real web open paths: navigate to /p/<project>/<tool>/<file>,
|
* These tests exercise the real web open paths: navigate to /p/<project>/<tool>/<file>,
|
||||||
* let WasmTool boot the tool in-document (boot.ts), drive the project into MEMFS
|
* let WasmTool boot the tool in-document (boot.ts), drive the project into MEMFS
|
||||||
* and auto-open the file (open-flow.ts via Module.kicadOpenFile), and assert the
|
* and auto-open the file (open-flow.ts via Module.kicadOpenFile), and assert the
|
||||||
* editor loaded wizard-free.
|
* editor loaded wizard-free.
|
||||||
*
|
*
|
||||||
* PREREQUISITE: the web stack must be running. From web/:
|
* The backend serves a single project off the local filesystem — no DB, no
|
||||||
* pnpm db:up && pnpm db:migrate && pnpm dev
|
* seeding. The webServer block reuses an already-running stack (`pnpm dev` from
|
||||||
* (db:migrate seeds the "demo" project; global-setup-web.ts re-seeds it through
|
* web/) or cold-starts it with the env below, pointing PROJECT_DIR at the
|
||||||
* the API if missing.) The webServer block below reuses an already-running stack
|
* committed tests/fixtures/demo project (slug "demo").
|
||||||
* and, if absent, best-effort starts `turbo dev` — but that still needs Postgres
|
|
||||||
* up (pnpm db:up) and migrated first.
|
|
||||||
*
|
*
|
||||||
* Firefox is the reliable headless target on ARM Mac (Chromium SwiftShader WebGL
|
* Firefox is the reliable headless target on ARM Mac (Chromium SwiftShader WebGL
|
||||||
* bug); use --project=chromium (system Chrome) for headed debugging.
|
* bug); use --project=chromium (system Chrome) for headed debugging.
|
||||||
|
|
@ -54,11 +53,18 @@ export default defineConfig({
|
||||||
|
|
||||||
webServer: {
|
webServer: {
|
||||||
// Best-effort: reuse the dev stack if it's already up (the common case);
|
// Best-effort: reuse the dev stack if it's already up (the common case);
|
||||||
// otherwise start turbo dev. Postgres (pnpm db:up) + db:migrate must already
|
// otherwise cold-start turbo dev with the env a fresh checkout needs
|
||||||
// have run — turbo dev does not provision the database.
|
// (turbo passes these through, so no hand-copied .env files required).
|
||||||
command: 'pnpm --dir ../web dev',
|
command: 'pnpm --dir ../web dev',
|
||||||
url: FRONTEND_URL,
|
url: FRONTEND_URL,
|
||||||
reuseExistingServer: true,
|
reuseExistingServer: true,
|
||||||
timeout: 120000,
|
timeout: 120000,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
// resolved against web/backend/ (the backend's cwd)
|
||||||
|
PROJECT_DIR: '../../tests/fixtures/demo',
|
||||||
|
VITE_API_BASE_URL: 'http://localhost:3060',
|
||||||
|
CORS_ORIGIN: 'http://localhost:3048',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,16 @@
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import type { FullConfig } from '@playwright/test';
|
import type { FullConfig } from '@playwright/test';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global setup for the web-app e2e suite.
|
* Global setup for the web-app e2e suite.
|
||||||
*
|
*
|
||||||
* The tests drive the real React app at :3048 and open files from the committed
|
* The stack is the standalone editor (:3048) plus the reference backend
|
||||||
* "demo" project. That project is normally created by `pnpm db:migrate`
|
* (:3060, @pcbjam/backend-example), which serves a single project off the
|
||||||
* (seedDemoProject). This setup makes the suite self-sufficient: it waits for
|
* local filesystem — PROJECT_DIR=tests/fixtures/demo, so the slug is "demo"
|
||||||
* the API to be reachable and, if the demo project is missing, recreates it by
|
* (basename of PROJECT_DIR). Nothing to seed: just wait for the backend and
|
||||||
* uploading the committed seed-data files through the public API — the same
|
* verify it serves the committed demo files the specs open.
|
||||||
* bytes db:seed uses. Idempotent.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const API_BASE = process.env.VITE_API_BASE_URL ?? 'http://localhost:3050';
|
const API_BASE = process.env.BACKEND_URL ?? 'http://localhost:3060';
|
||||||
const SEED_DIR = path.resolve(__dirname, '../../web/apps/server/seed-data');
|
|
||||||
const DEMO_SLUG = 'demo';
|
const DEMO_SLUG = 'demo';
|
||||||
const DEMO_FILES = ['demo.kicad_sch', 'demo.kicad_pcb', 'demo.kicad_wks'];
|
const DEMO_FILES = ['demo.kicad_sch', 'demo.kicad_pcb', 'demo.kicad_wks'];
|
||||||
|
|
||||||
|
|
@ -32,43 +28,30 @@ async function waitForApi(timeoutMs = 60000): Promise<void> {
|
||||||
await new Promise((res) => setTimeout(res, 1000));
|
await new Promise((res) => setTimeout(res, 1000));
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`API not reachable at ${API_BASE} (${lastErr}). Start the web stack first: ` +
|
`backend not reachable at ${API_BASE} (${lastErr}). Start the web stack first: ` +
|
||||||
`from web/ run \`pnpm db:up && pnpm db:migrate && pnpm dev\`.`
|
`from web/ run \`pnpm dev\` (PROJECT_DIR defaults to tests/fixtures/demo via ` +
|
||||||
|
`backend/.env or the playwright webServer env).`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureDemoProject(): Promise<void> {
|
async function verifyDemoProject(): Promise<void> {
|
||||||
const existing = await fetch(`${API_BASE}/api/projects/${DEMO_SLUG}`);
|
const r = await fetch(`${API_BASE}/api/projects/${DEMO_SLUG}`);
|
||||||
if (existing.ok) {
|
if (!r.ok) {
|
||||||
const body = (await existing.json()) as { files: { path: string }[] };
|
throw new Error(
|
||||||
const have = new Set(body.files.map((f) => f.path));
|
`GET /api/projects/${DEMO_SLUG} -> HTTP ${r.status}; ` +
|
||||||
if (DEMO_FILES.every((f) => have.has(f))) return;
|
`is the backend's PROJECT_DIR pointing at tests/fixtures/demo?`
|
||||||
} else {
|
);
|
||||||
// Create the project (ignore 409 if a race created it).
|
|
||||||
const created = await fetch(`${API_BASE}/api/projects`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'content-type': 'application/json' },
|
|
||||||
body: JSON.stringify({ name: 'Demo Project', slug: DEMO_SLUG }),
|
|
||||||
});
|
|
||||||
if (!created.ok && created.status !== 409) {
|
|
||||||
throw new Error(`failed to create demo project: HTTP ${created.status}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const body = (await r.json()) as { files: { path: string }[] };
|
||||||
// Upload the committed seed bytes (multipart field-name = project-relative path).
|
const have = new Set(body.files.map((f) => f.path));
|
||||||
const form = new FormData();
|
const missing = DEMO_FILES.filter((f) => !have.has(f));
|
||||||
for (const name of DEMO_FILES) {
|
if (missing.length) {
|
||||||
const buf = fs.readFileSync(path.join(SEED_DIR, name));
|
throw new Error(`demo project missing files: ${missing.join(', ')}`);
|
||||||
form.append(name, new Blob([buf]), name);
|
|
||||||
}
|
}
|
||||||
const up = await fetch(`${API_BASE}/api/projects/${DEMO_SLUG}/files`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: form,
|
|
||||||
});
|
|
||||||
if (!up.ok) throw new Error(`failed to seed demo files: HTTP ${up.status}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function globalSetup(_config: FullConfig): Promise<void> {
|
export default async function globalSetup(_config: FullConfig): Promise<void> {
|
||||||
await waitForApi();
|
await waitForApi();
|
||||||
await ensureDemoProject();
|
await verifyDemoProject();
|
||||||
|
console.log(`web e2e setup: backend at ${API_BASE} serving "${DEMO_SLUG}" — OK`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
87
tests/web/tool-switch.spec.ts
Normal file
87
tests/web/tool-switch.spec.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { test, expect, type Page } from '@playwright/test';
|
||||||
|
import { clickMenuBarItem, clickMenuItem } from '../e2e/utils/element-tracker';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool-switch e2e: eeschema Tools → "Switch to PCB Editor" (and the reverse)
|
||||||
|
* must navigate the browser to the other tool's URL.
|
||||||
|
*
|
||||||
|
* Native KiCad spawns a process for this via ExecuteFile (common/gestfich.cpp);
|
||||||
|
* the WASM build delegates to window.kicadWebOpenTool (WasmTool.tsx), which
|
||||||
|
* maps the MEMFS file path to the project-relative file and calls
|
||||||
|
* location.assign(/p/demo/<tool>/<file>). Each direction is a full page
|
||||||
|
* navigation followed by a fresh wasm boot — hence the generous timeouts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function waitForToolReady(page: Page, titleRe: RegExp): Promise<void> {
|
||||||
|
await expect(page.locator('#canvas')).toBeVisible({ timeout: 120000 });
|
||||||
|
await expect
|
||||||
|
.poll(() => page.title(), {
|
||||||
|
message: `editor never reached title ${titleRe}`,
|
||||||
|
timeout: 120000,
|
||||||
|
intervals: [1000],
|
||||||
|
})
|
||||||
|
.toMatch(titleRe);
|
||||||
|
// The menu helpers drive the rendered-element registry.
|
||||||
|
await page.waitForFunction(
|
||||||
|
() =>
|
||||||
|
!!(window as unknown as { wxElementRegistry?: { findAllRendered?: unknown } })
|
||||||
|
.wxElementRegistry,
|
||||||
|
null,
|
||||||
|
{ timeout: 30000 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function switchTool(
|
||||||
|
page: Page,
|
||||||
|
menuLabel: string,
|
||||||
|
expectedUrl: RegExp,
|
||||||
|
expectedTitle: RegExp
|
||||||
|
): Promise<void> {
|
||||||
|
expect(await clickMenuBarItem(page, 'Tools'), 'Tools menubar item clickable').toBe(true);
|
||||||
|
await page.waitForTimeout(400); // popup render settle, same as tests/kicad specs
|
||||||
|
|
||||||
|
expect(await clickMenuItem(page, menuLabel), `"${menuLabel}" menu item clickable`).toBe(true);
|
||||||
|
|
||||||
|
await page.waitForURL(expectedUrl, { timeout: 30000 });
|
||||||
|
await waitForToolReady(page, expectedTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('web app — tool switching', () => {
|
||||||
|
test('eeschema → Switch to PCB Editor navigates to pcbnew', async ({ page }) => {
|
||||||
|
test.setTimeout(420000); // two full wasm boots
|
||||||
|
|
||||||
|
await page.goto('/p/demo/eeschema/demo.kicad_sch');
|
||||||
|
await waitForToolReady(page, /demo — Schematic Editor/i);
|
||||||
|
|
||||||
|
await switchTool(
|
||||||
|
page,
|
||||||
|
'Switch to PCB Editor',
|
||||||
|
/\/p\/demo\/pcbnew\/demo\.kicad_pcb/,
|
||||||
|
/demo — PCB Editor/i
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.screenshot({
|
||||||
|
path: 'test-results/web-switch-sch-to-pcb.png',
|
||||||
|
scale: 'device',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pcbnew → Switch to Schematic Editor navigates to eeschema', async ({ page }) => {
|
||||||
|
test.setTimeout(420000);
|
||||||
|
|
||||||
|
await page.goto('/p/demo/pcbnew/demo.kicad_pcb');
|
||||||
|
await waitForToolReady(page, /demo — PCB Editor/i);
|
||||||
|
|
||||||
|
await switchTool(
|
||||||
|
page,
|
||||||
|
'Switch to Schematic Editor',
|
||||||
|
/\/p\/demo\/eeschema\/demo\.kicad_sch/,
|
||||||
|
/demo — Schematic Editor/i
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.screenshot({
|
||||||
|
path: 'test-results/web-switch-pcb-to-sch.png',
|
||||||
|
scale: 'device',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,14 +1,171 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { collabRoomId, type Tool } from "@pcbjam/shared";
|
import {
|
||||||
|
collabRoomId,
|
||||||
|
EXTENSION_TOOL,
|
||||||
|
FILELESS_TOOLS,
|
||||||
|
toolSchema,
|
||||||
|
type Tool,
|
||||||
|
} from "@pcbjam/shared";
|
||||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||||
import { WASM_ASSET_BASE_URL, yjsProviderConfig } from "@/lib/config";
|
import { WASM_ASSET_BASE_URL, yjsProviderConfig } from "@/lib/config";
|
||||||
import { bootKicadTool } from "@/wasm/boot";
|
import { bootKicadTool } from "@/wasm/boot";
|
||||||
|
import { memfsProjectDir } from "@/wasm/constants";
|
||||||
import { driveProjectIntoTool, type ToolFile } from "@/wasm/kicad-runner";
|
import { driveProjectIntoTool, type ToolFile } from "@/wasm/kicad-runner";
|
||||||
import type { CollabWindow } from "@/wasm/collab";
|
import type { CollabWindow } from "@/wasm/collab";
|
||||||
import { clog, cwarn } from "@/wasm/collab/debug";
|
import { clog, cwarn } from "@/wasm/collab/debug";
|
||||||
|
|
||||||
// Tools with a working collab bridge (kicadCollabSnapshot/Apply embind exports).
|
// Tools with a working collab bridge (kicadCollabSnapshot/Apply embind exports).
|
||||||
const COLLAB_TOOLS = new Set<Tool>(["pl_editor", "eeschema", "pcbnew"]);
|
const COLLAB_TOOLS = new Set<Tool>(["pl_editor", "eeschema", "pcbnew"]);
|
||||||
|
const LEGACY_EXTENSION_TOOL: Record<string, Tool> = {
|
||||||
|
".sch": "eeschema",
|
||||||
|
".brd": "pcbnew",
|
||||||
|
};
|
||||||
|
|
||||||
|
let activeToolNavigationHook:
|
||||||
|
| ((toolName: string, fileName: string) => boolean)
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const toolNavigationDispatcher = (toolName: string, fileName: string) =>
|
||||||
|
activeToolNavigationHook?.(toolName, fileName) ?? false;
|
||||||
|
|
||||||
|
function ensureToolNavigationDispatcher(win: ToolWindow): boolean {
|
||||||
|
if (win.kicadWebOpenTool === toolNavigationDispatcher) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Object.defineProperty(win, "kicadWebOpenTool", {
|
||||||
|
configurable: true,
|
||||||
|
value: toolNavigationDispatcher,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
ensureToolNavigationDispatcher(window as ToolWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeToolName(rawName: string): Tool | null {
|
||||||
|
const basename = rawName.replace(/\\/g, "/").split("/").pop() ?? rawName;
|
||||||
|
const withoutExe = basename.replace(/\.exe$/i, "");
|
||||||
|
const toolName = withoutExe === "pcb_calculator" ? "calculator" : withoutExe;
|
||||||
|
const parsed = toolSchema.safeParse(toolName);
|
||||||
|
return parsed.success ? parsed.data : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function relativeProjectPath(slug: string, path: string): string | undefined {
|
||||||
|
if (!path) return undefined;
|
||||||
|
|
||||||
|
const normalized = path.replace(/\\/g, "/");
|
||||||
|
const prefix = `${memfsProjectDir(slug)}/`;
|
||||||
|
|
||||||
|
if (normalized.startsWith(prefix)) return normalized.slice(prefix.length);
|
||||||
|
|
||||||
|
const marker = `/projects/${slug}/`;
|
||||||
|
const markerIndex = normalized.indexOf(marker);
|
||||||
|
|
||||||
|
if (markerIndex >= 0) return normalized.slice(markerIndex + marker.length);
|
||||||
|
|
||||||
|
return normalized.startsWith("/") ? undefined : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileStem(path: string): string {
|
||||||
|
const name = path.replace(/\\/g, "/").split("/").pop() ?? path;
|
||||||
|
return name.replace(/\.[^.]+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileTool(path: string): Tool | undefined {
|
||||||
|
const lower = path.toLowerCase();
|
||||||
|
|
||||||
|
for (const [extension, mappedTool] of Object.entries({
|
||||||
|
...EXTENSION_TOOL,
|
||||||
|
...LEGACY_EXTENSION_TOOL,
|
||||||
|
})) {
|
||||||
|
if (lower.endsWith(extension)) return mappedTool;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseToolFile(
|
||||||
|
files: ToolFile[],
|
||||||
|
nextTool: Tool,
|
||||||
|
requestedPath?: string,
|
||||||
|
currentPath?: string,
|
||||||
|
): string | undefined {
|
||||||
|
if (requestedPath && files.some((file) => file.path === requestedPath)) {
|
||||||
|
return requestedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidates = files.filter((file) => fileTool(file.path) === nextTool);
|
||||||
|
const preferredStem = requestedPath
|
||||||
|
? fileStem(requestedPath)
|
||||||
|
: currentPath
|
||||||
|
? fileStem(currentPath)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (preferredStem) {
|
||||||
|
const matchingStem = candidates.find(
|
||||||
|
(file) => fileStem(file.path) === preferredStem,
|
||||||
|
);
|
||||||
|
if (matchingStem) return matchingStem.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates[0]?.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeRelPath(path: string): string {
|
||||||
|
return path.split("/").map(encodeURIComponent).join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function installToolNavigationHook(
|
||||||
|
win: ToolWindow,
|
||||||
|
opts: {
|
||||||
|
slug: string;
|
||||||
|
files: ToolFile[];
|
||||||
|
targetPath?: string;
|
||||||
|
log: (m: string) => void;
|
||||||
|
},
|
||||||
|
): () => void {
|
||||||
|
const hook = (rawToolName: string, rawFileName: string): boolean => {
|
||||||
|
const nextTool = normalizeToolName(rawToolName);
|
||||||
|
|
||||||
|
if (!nextTool) {
|
||||||
|
opts.log(`[nav] unsupported KiCad tool: ${rawToolName}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestedPath = relativeProjectPath(opts.slug, rawFileName);
|
||||||
|
const nextPath = FILELESS_TOOLS.has(nextTool)
|
||||||
|
? undefined
|
||||||
|
: chooseToolFile(opts.files, nextTool, requestedPath, opts.targetPath);
|
||||||
|
|
||||||
|
if (!FILELESS_TOOLS.has(nextTool) && !nextPath) {
|
||||||
|
opts.log(`[nav] no project file found for ${nextTool}: ${rawFileName}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url =
|
||||||
|
`/p/${encodeURIComponent(opts.slug)}/${nextTool}` +
|
||||||
|
(nextPath ? `/${encodeRelPath(nextPath)}` : "") +
|
||||||
|
win.location.search;
|
||||||
|
|
||||||
|
opts.log(`[nav] ${rawToolName} ${rawFileName || "(no file)"} -> ${url}`);
|
||||||
|
win.location.assign(url);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!ensureToolNavigationDispatcher(win)) {
|
||||||
|
opts.log("[nav] unable to install KiCad tool navigation hook");
|
||||||
|
}
|
||||||
|
|
||||||
|
activeToolNavigationHook = hook;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (activeToolNavigationHook === hook) activeToolNavigationHook = undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collaborative editing (features/yjs-bridge), ON BY DEFAULT for any tool that has the
|
* Collaborative editing (features/yjs-bridge), ON BY DEFAULT for any tool that has the
|
||||||
|
|
@ -104,6 +261,21 @@ export function WasmTool({
|
||||||
const [showLog, setShowLog] = React.useState(false);
|
const [showLog, setShowLog] = React.useState(false);
|
||||||
|
|
||||||
const base = (assetBaseUrl ?? WASM_ASSET_BASE_URL).replace(/\/$/, "");
|
const base = (assetBaseUrl ?? WASM_ASSET_BASE_URL).replace(/\/$/, "");
|
||||||
|
const append = React.useCallback(
|
||||||
|
(msg: string) => setLogs((prev) => [...prev.slice(-800), msg]),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const removeNavigationHook = installToolNavigationHook(window as ToolWindow, {
|
||||||
|
slug,
|
||||||
|
files,
|
||||||
|
targetPath,
|
||||||
|
log: append,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => removeNavigationHook();
|
||||||
|
}, [slug, files, targetPath, append]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// Guard re-entry: the WASM runtime is process-global and must boot exactly
|
// Guard re-entry: the WASM runtime is process-global and must boot exactly
|
||||||
|
|
@ -117,8 +289,6 @@ export function WasmTool({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const append = (msg: string) =>
|
|
||||||
setLogs((prev) => [...prev.slice(-800), msg]);
|
|
||||||
const win = window as ToolWindow;
|
const win = window as ToolWindow;
|
||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
|
|
@ -142,7 +312,7 @@ export function WasmTool({
|
||||||
// Boot is one-shot per mount; deps intentionally exclude files/targetPath so
|
// Boot is one-shot per mount; deps intentionally exclude files/targetPath so
|
||||||
// they don't retrigger a (rejected) second boot.
|
// they don't retrigger a (rejected) second boot.
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [tool, slug, base]);
|
}, [tool, slug, base, append]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-screen w-screen overflow-hidden bg-[#1a1a2e]">
|
<div className="relative h-screen w-screen overflow-hidden bg-[#1a1a2e]">
|
||||||
|
|
|
||||||
1
web/standalone/src/wasm/global.d.ts
vendored
1
web/standalone/src/wasm/global.d.ts
vendored
|
|
@ -44,6 +44,7 @@ declare global {
|
||||||
Module?: any;
|
Module?: any;
|
||||||
FS?: EmscriptenFS;
|
FS?: EmscriptenFS;
|
||||||
wxElementRegistry?: WxElementRegistry;
|
wxElementRegistry?: WxElementRegistry;
|
||||||
|
kicadWebOpenTool?: (toolName: string, fileName: string) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The browsing-context window the tool runs in — now the top-level `window`
|
// The browsing-context window the tool runs in — now the top-level `window`
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue