pcbjam/features/pl-editor/root.patch
Balint Ipkovich d735779e23 feat: pl_editor WASM port + browser file dialog fixes
Brings up KiCad's pagelayout_editor (drawing-sheet editor) in the
browser, to roughly the same "boots, canvas visible, partially usable
in-session" level as the existing pcbnew/eeschema/calculator ports.

Build:
- docker/build.sh: add pl_editor to the unified app dispatch (case,
  subdir map, all-loop).
- scripts/kicad/build-kicad-target.sh: add pl_editor to the case;
  upstream target name pl_editor under source subdir pagelayout_editor.
- scripts/kicad/build-pl_editor.sh: 7-line thin wrapper matching the
  pcbnew/eeschema/calculator pattern.
- tests/scripts/setup-kicad-wasm.sh: copy_app pl_editor.

App glue:
- wasm/stubs/nl_pl_editor_plugin_stub.cpp: no-op SpaceMouse plugin so
  pl_editor_frame.cpp's NL_PL_EDITOR_PLUGIN symbols resolve. Mirrors
  nl_pcbnew_plugin_stub.cpp.
- tests/apps/kicad/pl_editor.html: browser shell. preRun creates
  /home/kicad and FS.chdir there so file dialogs land somewhere
  friendly instead of MEMFS root (/dev/, /proc/, etc.).

E2E coverage:
- tests/kicad/pl_editor.spec.ts: 5 tests — smoke (canvas, no abort),
  wizard, File menu has Open/Save As, file-dialog folder-navigation
  regression, canvas + toolbar metrics.
- tests/e2e/filedialog-folder-nav.spec.ts: wxWidgets-level twin of
  the regression test (exercises the underlying widget directly via
  the standalone filedialog_test app).

Submodule bumps:
- kicad → feature/pl-editor (WASM gating in pagelayout_editor's
  CMakeLists + navlib stub).
- wxwidgets → feature/pl-editor (wxGenericFileDialog::OnOk navigates
  into selected directories; wasm/mouse.cpp emits wxEVT_LEFT_DCLICK
  via timestamp-based double-click detection — the latter benefits
  every wxWidgets-WASM app).

See features/pl-editor/ for the design doc + per-repo diff patches.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 10:30:56 +02:00

621 lines
23 KiB
Diff

diff --git a/docker/build.sh b/docker/build.sh
index 85a9115..f5da04e 100755
--- a/docker/build.sh
+++ b/docker/build.sh
@@ -29,7 +29,7 @@ set -e
cd "$(dirname "$0")/.."
-VALID_APPS="pcbnew | eeschema | calculator | all"
+VALID_APPS="pcbnew | eeschema | calculator | pl_editor | all"
usage() {
echo "Usage: ./docker/build.sh <app> [args...]" >&2
@@ -52,7 +52,7 @@ APP_NAME="$1"
shift
case "$APP_NAME" in
- pcbnew|eeschema|calculator|all) ;;
+ pcbnew|eeschema|calculator|pl_editor|all) ;;
*)
echo "Error: unknown app '$APP_NAME' (expected: ${VALID_APPS})" >&2
usage
@@ -115,6 +115,7 @@ fi
kicad_subdir_for() {
case "$1" in
calculator) echo "pcb_calculator" ;;
+ pl_editor) echo "pagelayout_editor" ;;
*) echo "$1" ;;
esac
}
@@ -158,6 +159,7 @@ if [[ "${APP_NAME}" == "all" ]]; then
build_app pcbnew
build_app eeschema
build_app calculator
+ build_app pl_editor
else
build_app "${APP_NAME}"
fi
diff --git a/scripts/kicad/build-kicad-target.sh b/scripts/kicad/build-kicad-target.sh
index c89b2ea..aaf00fd 100755
--- a/scripts/kicad/build-kicad-target.sh
+++ b/scripts/kicad/build-kicad-target.sh
@@ -1,11 +1,11 @@
#!/bin/bash
-# Build a KiCad app (pcbnew, eeschema, calculator) for WebAssembly.
+# Build a KiCad app (pcbnew, eeschema, calculator, pl_editor) for WebAssembly.
#
# Usage:
# ./scripts/kicad/build-kicad-target.sh <app> [options]
#
# Args:
-# <app> pcbnew | eeschema | calculator (required)
+# <app> pcbnew | eeschema | calculator | pl_editor (required)
#
# Options:
# --full Full clean rebuild (dependencies + KiCad)
@@ -26,25 +26,26 @@
# the source subdirectory. Calculator is the exception: app=calculator but the
# upstream target and source subdir are both pcb_calculator (the OUTPUT_NAME
# property in pcb_calculator/CMakeLists.txt emits calculator.{js,wasm}).
+# pl_editor is the standard case but its source subdir is pagelayout_editor.
set -e
if [ -z "$1" ]; then
- echo "Error: missing <app> argument (pcbnew | eeschema | calculator)" >&2
+ echo "Error: missing <app> argument (pcbnew | eeschema | calculator | pl_editor)" >&2
exit 1
fi
APP_NAME="$1"
shift
case "$APP_NAME" in
- pcbnew|eeschema)
+ pcbnew|eeschema|pl_editor)
KICAD_TARGET="$APP_NAME"
;;
calculator)
KICAD_TARGET="pcb_calculator"
;;
*)
- echo "Error: unknown app '$APP_NAME' (expected: pcbnew | eeschema | calculator)" >&2
+ echo "Error: unknown app '$APP_NAME' (expected: pcbnew | eeschema | calculator | pl_editor)" >&2
exit 1
;;
esac
diff --git a/scripts/kicad/build-pl_editor.sh b/scripts/kicad/build-pl_editor.sh
new file mode 100755
index 0000000..17315f1
--- /dev/null
+++ b/scripts/kicad/build-pl_editor.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+# Build KiCad pl_editor (drawing-sheet editor) for WebAssembly.
+# Thin wrapper around build-kicad-target.sh — see that script for options.
+
+set -e
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+exec "${SCRIPT_DIR}/build-kicad-target.sh" pl_editor "$@"
diff --git a/tests/apps/kicad/pl_editor.html b/tests/apps/kicad/pl_editor.html
new file mode 100644
index 0000000..516ecca
--- /dev/null
+++ b/tests/apps/kicad/pl_editor.html
@@ -0,0 +1,200 @@
+<!DOCTYPE html>
+<html lang="en-us">
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <title>KiCad Page Layout Editor WASM</title>
+ <style>
+ .emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; }
+ div.emscripten { text-align: center; }
+ /* the canvas *must not* have any border or padding, or mouse coords will be wrong */
+ canvas.emscripten { border: 0px none; }
+
+ .window {
+ position: absolute;
+ pointer-events: none;
+ z-index: 10;
+ background-color: black;
+ overflow: hidden;
+ width: 0;
+ height: 0;
+ }
+
+ .window-canvas {
+ position: absolute;
+ top: 0;
+ left: 0;
+ pointer-events: none;
+ }
+
+ #status {
+ position: fixed;
+ bottom: 10px;
+ left: 10px;
+ color: #fff;
+ font-family: monospace;
+ z-index: 1000;
+ background: rgba(0,0,0,0.7);
+ padding: 10px;
+ border-radius: 5px;
+ }
+
+ #progress {
+ width: 300px;
+ height: 20px;
+ background: #333;
+ margin-top: 5px;
+ }
+
+ #progress-bar {
+ height: 100%;
+ background: #4CAF50;
+ width: 0%;
+ transition: width 0.3s;
+ }
+ </style>
+</head>
+<body style="margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #1a1a2e;">
+ <div id="main-window" style="width: 100vw; height: 100vh; position: absolute; top: 0; left: 0;"></div>
+
+ <div id="status">
+ <div id="status-text">Initializing...</div>
+ <div id="progress"><div id="progress-bar"></div></div>
+ </div>
+
+ <div id="window-container"></div>
+
+ <script>
+ var mainWindow = document.getElementById('main-window');
+ var statusText = document.getElementById('status-text');
+ var progressBar = document.getElementById('progress-bar');
+
+ var showError = function(msg) {
+ console.error('[KICAD_ERROR] ' + msg);
+ statusText.textContent = 'Error: ' + msg;
+ statusText.style.color = 'red';
+ };
+
+ var createCanvas = function() {
+ var canvas = document.createElement('canvas');
+ canvas.id = 'canvas';
+ canvas.style.display = 'none';
+ var width = window.innerWidth;
+ var height = window.innerHeight;
+ canvas.style.width = width + 'px';
+ canvas.style.height = height + 'px';
+ canvas.oncontextmenu = function() { event.preventDefault(); };
+ canvas.addEventListener("webglcontextlost", function(e) {
+ showError('WebGL context lost. You will need to reload the page.');
+ e.preventDefault();
+ }, false);
+
+ mainWindow.appendChild(canvas);
+ Module.canvas = canvas;
+
+ console.log('[KICAD] preRun complete, canvas created: ' + width + 'x' + height);
+ };
+
+ var onRuntimeInitialized = function() {
+ console.log('[KICAD] Runtime initialized');
+ var canvas = Module.canvas;
+ canvas.style.display = 'block';
+ document.getElementById('status').style.display = 'none';
+ };
+
+ // Pre-fetched resource data (fetched before pl_editor.js loads)
+ var resourceData = null;
+
+ fetch('images.tar.gz')
+ .then(function(response) {
+ if (!response.ok) throw new Error('HTTP ' + response.status);
+ return response.arrayBuffer();
+ })
+ .then(function(buffer) {
+ resourceData = new Uint8Array(buffer);
+ console.log('[KICAD] Prefetched images.tar.gz (' + resourceData.length + ' bytes)');
+ })
+ .catch(function(err) {
+ console.warn('[KICAD] Could not prefetch images.tar.gz:', err.message);
+ });
+
+ var writeResources = function() {
+ var resourcePath = '/workspace/build-wasm/sysroot/share/kicad/resources';
+ FS.mkdirTree(resourcePath);
+
+ if (resourceData) {
+ FS.writeFile(resourcePath + '/images.tar.gz', resourceData);
+ console.log('[KICAD] Wrote images.tar.gz to ' + resourcePath);
+ } else {
+ console.warn('[KICAD] images.tar.gz not ready yet (WASM loaded faster than fetch)');
+ }
+ };
+
+ // Land the user in a sane, writable directory instead of MEMFS root (where
+ // the only visible entries are /dev, /proc, /tmp, /workspace, /home).
+ // The Open/Save dialogs default to cwd when no explicit defaultDir is set.
+ var setupHomeDir = function() {
+ var home = '/home/kicad';
+ FS.mkdirTree(home);
+ FS.chdir(home);
+ console.log('[KICAD] cwd set to ' + home);
+ };
+
+ var Module = {
+ thisProgram: '/usr/bin/pl_editor', // Fake absolute path for argv[0]
+
+ preRun: [createCanvas, writeResources, setupHomeDir],
+ postRun: [],
+
+ print: function(text) {
+ if (arguments.length > 1)
+ text = Array.prototype.slice.call(arguments).join(' ');
+ console.log('[KICAD_OUT] ' + text);
+ },
+
+ printErr: function(text) {
+ if (arguments.length > 1)
+ text = Array.prototype.slice.call(arguments).join(' ');
+ console.error('[KICAD_ERR] ' + text);
+ },
+
+ setStatus: function(text) {
+ console.log('[KICAD_STATUS] ' + text);
+ statusText.textContent = text;
+
+ var match = text.match(/(\d+)\/(\d+)/);
+ if (match) {
+ var pct = (parseInt(match[1]) / parseInt(match[2])) * 100;
+ progressBar.style.width = pct + '%';
+ }
+ },
+
+ totalDependencies: 0,
+ monitorRunDependencies: function(left) {
+ this.totalDependencies = Math.max(this.totalDependencies, left);
+ Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
+ },
+
+ onRuntimeInitialized: onRuntimeInitialized,
+
+ locateFile: function(path) {
+ return path;
+ }
+ };
+
+ Module.setStatus('Downloading...');
+
+ window.onerror = function(msg, url, line) {
+ showError(msg + ' at ' + url + ':' + line);
+ Module.setStatus = function(text) {
+ if (text) Module.printErr('[post-exception status] ' + text);
+ };
+ return false;
+ };
+ </script>
+
+ <!-- wxWidgets WASM glue code (defines getConfigEntryLength, etc.) -->
+ <script src="wx.js"></script>
+ <script async src="pl_editor.js"></script>
+</body>
+</html>
diff --git a/tests/e2e/filedialog-folder-nav.spec.ts b/tests/e2e/filedialog-folder-nav.spec.ts
new file mode 100644
index 0000000..89d0720
--- /dev/null
+++ b/tests/e2e/filedialog-folder-nav.spec.ts
@@ -0,0 +1,37 @@
+// Regression coverage for the wxFileDialog folder-navigation fix
+// (wxGenericFileDialog::OnOk now navigates into directories instead of
+// closing the dialog with the folder path as a "file").
+//
+// Reproduces the original bug: select a folder, press Enter, expect the
+// dialog to navigate into the folder rather than close.
+
+import { test, expect, tryLoadApp, waitForRegistry, clickByLabel } from './utils/fixtures';
+
+test('folder navigation: Enter on a folder navigates instead of closing the dialog', async ({ page, testLogger }) => {
+ await page.goto('/standalone/filedialog/filedialog_test.html');
+ const loaded = await tryLoadApp(page);
+ expect(loaded, 'filedialog_test should load').toBe(true);
+
+ await waitForRegistry(page);
+
+ await clickByLabel(page, 'Open File...');
+ await page.waitForTimeout(800);
+
+ // Type a path that's a folder in Emscripten's MEMFS and press Enter.
+ // Before the fix, OnOk treated /dev as a file → either showed "Please
+ // choose an existing file" (wxFD_FILE_MUST_EXIST) or closed the dialog
+ // and surfaced /dev to the calling app as if it were a file.
+ await page.keyboard.type('/dev');
+ await page.waitForTimeout(200);
+ await page.keyboard.press('Enter');
+ await page.waitForTimeout(800);
+
+ await page.screenshot({ path: 'test-results/filedlg-folder-nav.png', fullPage: true });
+
+ // No "Selected file:" log should appear — the dialog must NOT have closed
+ // with /dev as the picked file.
+ const closedWithDev = testLogger.consoleLogs.some(l =>
+ l.includes('[FILEDIALOG_EVENT] Selected file:') && l.includes('/dev')
+ );
+ expect(closedWithDev, 'dialog must not close and report /dev as the selected file').toBe(false);
+});
diff --git a/tests/kicad/pl_editor.spec.ts b/tests/kicad/pl_editor.spec.ts
new file mode 100644
index 0000000..39084c5
--- /dev/null
+++ b/tests/kicad/pl_editor.spec.ts
@@ -0,0 +1,202 @@
+import type { Page } from '@playwright/test';
+import { test, expect } from './fixtures';
+import {
+ clickByLabel,
+ clickMenuBarItem,
+ clickMenuItem,
+} from '../e2e/utils/element-tracker';
+
+/**
+ * pl_editor (drawing-sheet editor) WASM E2E Tests
+ *
+ * Mirrors eeschema.spec.ts. Smoke + the wxFileDialog folder-navigation
+ * regression we fixed at the wxWidgets level (filedlgg.cpp). The widget-level
+ * coverage lives in tests/e2e/filedialog-folder-nav.spec.ts; this file proves
+ * the fix also works through pl_editor's own File menu.
+ */
+
+async function completeWizard(page: Page): Promise<void> {
+ await expect(page.locator('#canvas')).toBeVisible({ timeout: 90000 });
+ await page.waitForFunction(() => !!window.wxElementRegistry, null, { timeout: 90000 });
+ await page.waitForTimeout(2000);
+
+ await page.screenshot({ path: 'test-results/pl_editor-wizard-00-initial.png', scale: 'device' });
+
+ for (let i = 1; i <= 10; i++) {
+ let clicked = await clickByLabel(page, 'Next >');
+
+ if (!clicked) {
+ clicked = await clickByLabel(page, 'Finish');
+
+ if (clicked) {
+ await page.waitForTimeout(500);
+ await page.screenshot({
+ path: `test-results/pl_editor-wizard-${String(i).padStart(2, '0')}-finish.png`,
+ scale: 'device'
+ });
+ }
+
+ break;
+ }
+
+ await page.waitForTimeout(500);
+ await page.screenshot({
+ path: `test-results/pl_editor-wizard-${String(i).padStart(2, '0')}.png`,
+ scale: 'device'
+ });
+ }
+
+ await page.waitForTimeout(2000);
+}
+
+function hasAbort(testLogger: { consoleLogs: string[]; errors: string[] }): boolean {
+ return [...testLogger.consoleLogs, ...testLogger.errors].some(line => line.includes('Aborted('));
+}
+
+test.describe('pl_editor WASM', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/kicad/pl_editor.html');
+ });
+
+ test('app loads, canvas visible, no WASM abort', async ({ page, testLogger }) => {
+ await expect(page.locator('#canvas')).toBeVisible({ timeout: 90000 });
+ await page.waitForFunction(() => !!window.wxElementRegistry, null, { timeout: 90000 });
+ await page.waitForTimeout(1500);
+ await page.screenshot({ path: 'test-results/pl_editor-01-loaded.png', scale: 'device' });
+
+ expect(hasAbort(testLogger), 'no WASM abort during load').toBe(false);
+
+ const canvasCount = await page.locator('canvas').count();
+ expect(canvasCount).toBeGreaterThan(0);
+ });
+
+ test('wizard completes and leaves the editor in a clean state', async ({ page, testLogger }) => {
+ await completeWizard(page);
+
+ // After the wizard, no wxDialog/wxWizard should still be visible.
+ const blockingDialogs = await page.evaluate(() => {
+ const registry = window.wxElementRegistry;
+ if (!registry) return -1;
+ return registry.findAll({ visible: true })
+ .filter((el: { typeName: string }) =>
+ /^wxDialog|Wizard/.test(el.typeName))
+ .length;
+ });
+ expect(blockingDialogs, 'no blocking dialog/wizard visible after completeWizard()').toBe(0);
+ expect(hasAbort(testLogger), 'no WASM abort during wizard').toBe(false);
+
+ await page.screenshot({ path: 'test-results/pl_editor-02-post-wizard.png', scale: 'device' });
+ });
+
+ test('File menu exposes Open... and Save As...', async ({ page, testLogger }) => {
+ await completeWizard(page);
+
+ const fileMenuClicked = await clickMenuBarItem(page, 'File');
+ expect(fileMenuClicked, 'File menubar item should be clickable').toBe(true);
+ await page.waitForTimeout(400);
+
+ await page.screenshot({ path: 'test-results/pl_editor-03-file-menu.png', scale: 'device' });
+
+ // Menu items are tracked in the "rendered" half of the registry (popup
+ // widgets), not the regular findAll({visible:true}) set. Use findAllRendered
+ // and filter to menuitem elementType — same pattern as load-pcb-probe.spec.ts.
+ const menuLabels = await page.evaluate(() => {
+ const registry = window.wxElementRegistry;
+ if (!registry || !registry.findAllRendered) return [];
+ return registry.findAllRendered({})
+ .filter((r: { elementType: string }) => r.elementType === 'menuitem')
+ .map((r: { label?: string }) => r.label || '')
+ .filter((l: string) => l.length > 0);
+ });
+
+ // wxWidgets labels typically end with "..." (three ASCII dots) but some
+ // builds use the Unicode horizontal ellipsis "…". Accept either.
+ const hasOpen = menuLabels.some(l => /^Open[\.…]/.test(l) || l === 'Open');
+ const hasSaveAs = menuLabels.some(l => /^Save As[\.…]/.test(l) || l === 'Save As');
+ expect(hasOpen, `menu should contain "Open..." (saw labels: ${menuLabels.slice(0, 30).join(', ')})`).toBe(true);
+ expect(hasSaveAs, `menu should contain "Save As..." (saw labels: ${menuLabels.slice(0, 30).join(', ')})`).toBe(true);
+
+ // Dismiss the menu so we don't leak state into the next test.
+ await page.keyboard.press('Escape');
+ await page.waitForTimeout(200);
+
+ expect(hasAbort(testLogger)).toBe(false);
+ });
+
+ test('Save As file dialog: typing a folder + Enter navigates into it (regression)', async ({ page, testLogger }) => {
+ await completeWizard(page);
+
+ // Open File > Save As
+ await clickMenuBarItem(page, 'File');
+ await page.waitForTimeout(300);
+ const savedAsClicked = await clickMenuItem(page, 'Save As...');
+ expect(savedAsClicked, 'Save As... menu item should be clickable').toBe(true);
+
+ // Wait for the wxFileDialog to appear in the registry.
+ await page.waitForFunction(() => {
+ const registry = window.wxElementRegistry;
+ if (!registry) return false;
+ return registry.findAll({ visible: true })
+ .some((el: { typeName: string }) => el.typeName === 'wxFileDialog');
+ }, null, { timeout: 15000 });
+
+ await page.screenshot({ path: 'test-results/pl_editor-04-save-as-dialog.png', scale: 'device' });
+
+ // The bug: pressing Enter on a folder name treated it as a file and surfaced
+ // "Unable to load /dev file". After the OnOk fix, the dialog should navigate
+ // into the folder instead.
+ await page.keyboard.type('/dev');
+ await page.waitForTimeout(200);
+ await page.keyboard.press('Enter');
+ await page.waitForTimeout(900);
+
+ await page.screenshot({ path: 'test-results/pl_editor-04b-after-enter.png', scale: 'device' });
+
+ // The wxFileDialog should still be visible — we navigated into /dev, didn't close it.
+ const dialogStillOpen = await page.evaluate(() => {
+ const registry = window.wxElementRegistry;
+ if (!registry) return false;
+ return registry.findAll({ visible: true })
+ .some((el: { typeName: string }) => el.typeName === 'wxFileDialog');
+ });
+ expect(dialogStillOpen, 'wxFileDialog should remain open after Enter on a folder').toBe(true);
+
+ // The pre-fix error path surfaced "Unable to load <path> file" through KiCad's
+ // logger when the folder was returned as a "file". Ensure it didn't fire.
+ const unableToLoad = testLogger.consoleLogs.some(l => /Unable to load.*\/dev/.test(l));
+ expect(unableToLoad, 'KiCad must not surface "Unable to load /dev file"').toBe(false);
+
+ // Close the dialog cleanly so it doesn't leak to a subsequent step.
+ await page.keyboard.press('Escape');
+ await page.waitForTimeout(300);
+
+ expect(hasAbort(testLogger)).toBe(false);
+ });
+
+ test('canvas + toolbar metrics look sane', async ({ page, testLogger }) => {
+ await completeWizard(page);
+
+ const metrics = await page.evaluate(() => {
+ const registry = window.wxElementRegistry;
+ const all = registry ? registry.findAll({ visible: true }) : [];
+ const toolbars = all.filter((el: { typeName: string }) => /ToolBar/.test(el.typeName));
+ const glCanvas = document.querySelector('canvas[id*="gl"]') as HTMLCanvasElement | null;
+
+ return {
+ registryTotal: all.length,
+ toolbarCount: toolbars.length,
+ mainCanvasOk: (() => {
+ const c = document.getElementById('canvas') as HTMLCanvasElement | null;
+ return !!c && c.width > 0 && c.height > 0;
+ })(),
+ glCanvasOk: !!glCanvas && glCanvas.width > 0 && glCanvas.height > 0,
+ };
+ });
+
+ expect(metrics.registryTotal, 'registry should be populated').toBeGreaterThan(10);
+ expect(metrics.toolbarCount, 'at least one toolbar should be visible').toBeGreaterThanOrEqual(1);
+ expect(metrics.mainCanvasOk, 'main canvas has nonzero dimensions').toBe(true);
+ expect(metrics.glCanvasOk, 'GL canvas has nonzero dimensions').toBe(true);
+ expect(hasAbort(testLogger)).toBe(false);
+ });
+});
diff --git a/tests/scripts/setup-kicad-wasm.sh b/tests/scripts/setup-kicad-wasm.sh
index aa79e89..ae5cdff 100755
--- a/tests/scripts/setup-kicad-wasm.sh
+++ b/tests/scripts/setup-kicad-wasm.sh
@@ -17,10 +17,12 @@ mkdir -p "$KICAD_TEST"
# Map an app name to its inner CMake build subdirectory. Most apps share their
# subdir name with the app name; pcb_calculator emits OUTPUT_NAME=calculator
-# but lives under the pcb_calculator/ subtree of the build dir.
+# but lives under the pcb_calculator/ subtree of the build dir, and pl_editor's
+# source lives under pagelayout_editor/.
kicad_subdir_for() {
case "$1" in
calculator) echo "pcb_calculator" ;;
+ pl_editor) echo "pagelayout_editor" ;;
*) echo "$1" ;;
esac
}
@@ -64,9 +66,10 @@ found_any=0
copy_app pcbnew && found_any=1
copy_app eeschema && found_any=1
copy_app calculator && found_any=1
+copy_app pl_editor && found_any=1
if [ "$found_any" -eq 0 ]; then
- echo "Error: no pcbnew/eeschema/calculator artifacts found in output/ or docker volume" >&2
+ echo "Error: no pcbnew/eeschema/calculator/pl_editor artifacts found in output/ or docker volume" >&2
exit 1
fi
diff --git a/wasm/stubs/nl_pl_editor_plugin_stub.cpp b/wasm/stubs/nl_pl_editor_plugin_stub.cpp
new file mode 100644
index 0000000..d3fe4df
--- /dev/null
+++ b/wasm/stubs/nl_pl_editor_plugin_stub.cpp
@@ -0,0 +1,29 @@
+/*
+ * 3Dconnexion SpaceMouse plugin stubs for KiCad pagelayout_editor WASM build.
+ * The 3DxWare driver is unavailable in the browser; these stubs satisfy the
+ * symbols referenced from pl_editor_frame.cpp without doing anything.
+ */
+
+// Minimal definition for NL_PL_EDITOR_PLUGIN_IMPL — required because the
+// unique_ptr<NL_PL_EDITOR_PLUGIN_IMPL> destructor needs a complete type.
+class NL_PL_EDITOR_PLUGIN_IMPL {};
+
+#include <navlib/nl_pl_editor_plugin.h>
+
+NL_PL_EDITOR_PLUGIN::NL_PL_EDITOR_PLUGIN()
+{
+}
+
+NL_PL_EDITOR_PLUGIN::~NL_PL_EDITOR_PLUGIN()
+{
+}
+
+void NL_PL_EDITOR_PLUGIN::SetCanvas( EDA_DRAW_PANEL_GAL* aViewport )
+{
+ (void) aViewport;
+}
+
+void NL_PL_EDITOR_PLUGIN::SetFocus( bool aFocus )
+{
+ (void) aFocus;
+}