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 [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/features/pl-editor/0001-pl-editor-port.md b/features/pl-editor/0001-pl-editor-port.md new file mode 100644 index 0000000..6d898f5 --- /dev/null +++ b/features/pl-editor/0001-pl-editor-port.md @@ -0,0 +1,54 @@ +# pl_editor (drawing-sheet editor) WASM port — design notes + +## Goal + +Bring up KiCad's `pagelayout_editor` sub-app (also known as `pl_editor`, the drawing-sheet editor) in the browser, to the same "boots, canvas visible, partially usable in-session" level as `pcbnew`. Persistence across sessions not required. + +## Approach + +Mirrors the in-tree pattern that pcbnew/calculator/eeschema use: gate WASM differences behind `if( EMSCRIPTEN )` blocks inside the upstream `pagelayout_editor/CMakeLists.txt`, keeping a single source of truth for the build alongside KiCad's existing platform conditionals (`if( WIN32 )`, `if( APPLE )`). + +(An earlier iteration tried an out-of-tree CMake wrapper to keep the kicad submodule bit-for-bit upstream. It worked, but diverged from the team norm — every other WASM-ported app modifies kicad. We converged to the team pattern; the only kicad-side cost is a ~80-line patch in this app's CMakeLists.txt, all WASM-conditional.) + +## Changes (see kicad.patch + root.patch + wxwidgets.patch) + +### kicad submodule + +- **`kicad/pagelayout_editor/CMakeLists.txt`** — mirrors pcbnew's WASM static-linking pattern: + - Drop `BUILD_KIWAY_DLL` from `single_top.cpp` and `pl_editor.cpp` compile defs on EMSCRIPTEN (browser can't `dlopen` a `.kiface` shared library). + - Split `pl_editor_kiface` into an OBJECT library (`pl_editor_kiface_objects`) + an empty MODULE; lets the same compiled objects be linked statically into the exe on WASM and dynamically into the `.kiface` module on native. + - On EMSCRIPTEN, link `pl_editor` directly against `PL_EDITOR_KIFACE_LIBRARIES` with `LINKER:--allow-multiple-definition` (handles wxWidgets/nanosvg duplicate symbols, same as pcbnew). +- **`kicad/pagelayout_editor/navlib/CMakeLists.txt`** — for EMSCRIPTEN, replace the real 3Dconnexion SpaceMouse plugin sources with `wasm/stubs/nl_pl_editor_plugin_stub.cpp` (no USB hardware in the browser). + +### Root repo + +- **`wasm/stubs/nl_pl_editor_plugin_stub.cpp`** — no-op `NL_PL_EDITOR_PLUGIN` ctor/dtor + `SetCanvas`/`SetFocus`, mirroring `nl_pcbnew_plugin_stub.cpp`. +- **`scripts/kicad/build-pl_editor.sh`** — thin wrapper around `build-kicad-target.sh pl_editor`. +- **`scripts/kicad/build-kicad-target.sh`** — adds `pl_editor` to the `case` (uses upstream target name `pl_editor`, source subdir `pagelayout_editor`). +- **`docker/build.sh`** — adds `pl_editor` to the unified app dispatch (valid apps + `all` loop + `kicad_subdir_for`). +- **`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. +- **`tests/scripts/setup-kicad-wasm.sh`** — `copy_app pl_editor` added to the existing list. +- **`tests/e2e/filedialog-folder-nav.spec.ts`** — regression test for the wxFileDialog folder-navigation fix. + +### wxwidgets submodule (file dialog usability fixes) + +These were discovered while bringing up pl_editor's file dialog but apply to any wxWidgets-WASM app: + +- **`wxwidgets/src/generic/filedlgg.cpp`** — `wxGenericFileDialog::OnOk` now navigates into the selected entry when it's a directory instead of closing the dialog and surfacing the folder path to the caller as if it were a file. Without this, KiCad's "Open Drawing Sheet" produced "Unable to load /dev file" when the user selected `/dev` (a directory in MEMFS). +- **`wxwidgets/src/wasm/mouse.cpp`** — stateful double-click detection. `EmscriptenMouseEvent` has no click-count field, so `wxEVT_LEFT_DCLICK` literally never fired in the WASM build — breaking `EVT_LIST_ITEM_ACTIVATED` on every listctrl. Now two MOUSEDOWNs of the same button within 500ms emit DCLICK. + +## Build & verify + +``` +./docker/build.sh pl_editor +./tests/scripts/setup-kicad-wasm.sh +# serve tests/apps/kicad and open pl_editor.html +``` + +Expect: window opens, canvas renders, File > Open / Save As dialogs work (folder navigation via single-click + Enter, single-click + OK, or double-click). Dialog lands at `/home/kicad` by default. + +## Known limitations + +- No persistent storage (MEMFS only); files vanish on tab close. +- No keyboard accelerator for "navigate to parent directory" beyond the up-arrow button + ".." entry. +- Drawing-sheet-specific tooling beyond basic edit/save is untested (out of MVP scope). diff --git a/features/pl-editor/kicad.patch b/features/pl-editor/kicad.patch new file mode 100644 index 0000000..b0b158f --- /dev/null +++ b/features/pl-editor/kicad.patch @@ -0,0 +1,194 @@ +diff --git a/pagelayout_editor/CMakeLists.txt b/pagelayout_editor/CMakeLists.txt +index f287e5368a..dfc01a9b9c 100644 +--- a/pagelayout_editor/CMakeLists.txt ++++ b/pagelayout_editor/CMakeLists.txt +@@ -77,55 +77,103 @@ add_executable( pl_editor WIN32 MACOSX_BUNDLE + ${PL_EDITOR_RESOURCES} + ) + +-set_source_files_properties( ${CMAKE_SOURCE_DIR}/common/single_top.cpp PROPERTIES +- COMPILE_DEFINITIONS "TOP_FRAME=FRAME_PL_EDITOR;PGM_DATA_FILE_EXT=\"kicad_wks\";BUILD_KIWAY_DLL" +- ) +-target_link_libraries( pl_editor +- kicommon +- ${wxWidgets_LIBRARIES} +- ) ++if( EMSCRIPTEN ) ++ # WASM: Static kiface linking - don't define BUILD_KIWAY_DLL ++ set_source_files_properties( ${CMAKE_SOURCE_DIR}/common/single_top.cpp PROPERTIES ++ COMPILE_DEFINITIONS "TOP_FRAME=FRAME_PL_EDITOR;PGM_DATA_FILE_EXT=\"kicad_wks\"" ++ ) ++else() ++ set_source_files_properties( ${CMAKE_SOURCE_DIR}/common/single_top.cpp PROPERTIES ++ COMPILE_DEFINITIONS "TOP_FRAME=FRAME_PL_EDITOR;PGM_DATA_FILE_EXT=\"kicad_wks\";BUILD_KIWAY_DLL" ++ ) ++endif() ++ ++if( NOT EMSCRIPTEN ) ++ # Native: minimal link, kiface loaded dynamically ++ target_link_libraries( pl_editor ++ kicommon ++ ${wxWidgets_LIBRARIES} ++ ) ++endif() ++# WASM linking is done after PL_EDITOR_KIFACE_LIBRARIES is defined + + target_link_options( pl_editor PRIVATE + $<$:-Wl,--cref,-Map=pl_editor.map> + ) + +-# the main pl_editor program, in DSO form. +-add_library( pl_editor_kiface MODULE ++# the main pl_editor program, in OBJECT form so it can be statically linked on WASM. ++add_library( pl_editor_kiface_objects OBJECT + pl_editor.cpp + ${PL_EDITOR_SRCS} + ${DIALOGS_SRCS} + ${PL_EDITOR_EXTRA_SRCS} + ) +-target_link_libraries( pl_editor_kiface +- gal +- common +- core +- ${wxWidgets_LIBRARIES} ++ ++target_link_libraries( pl_editor_kiface_objects ++ PRIVATE ++ common ++ core ++ ${wxWidgets_LIBRARIES} + ) ++ ++add_library( pl_editor_kiface MODULE ) ++ + set_target_properties( pl_editor_kiface PROPERTIES + OUTPUT_NAME pl_editor + PREFIX ${KIFACE_PREFIX} + SUFFIX ${KIFACE_SUFFIX} + ) + +-set_source_files_properties( pl_editor.cpp PROPERTIES +- # The KIFACE is in pcbnew.cpp, export it: +- COMPILE_DEFINITIONS "BUILD_KIWAY_DLL;COMPILING_DLL" +- ) ++if( EMSCRIPTEN ) ++ # WASM: Static linking - don't define BUILD_KIWAY_DLL ++ set_source_files_properties( pl_editor.cpp PROPERTIES ++ COMPILE_DEFINITIONS "COMPILING_DLL" ++ ) ++else() ++ set_source_files_properties( pl_editor.cpp PROPERTIES ++ # The KIFACE is in pl_editor.cpp, export it: ++ COMPILE_DEFINITIONS "BUILD_KIWAY_DLL;COMPILING_DLL" ++ ) ++endif() + + target_link_options( pl_editor_kiface PRIVATE + $<$:-Wl,--cref,-Map=_pl_editor.kiface.map> + ) + +-# if building pl_editor, then also build pl_editor_kiface if out of date. +-add_dependencies( pl_editor pl_editor_kiface ) +- + message( STATUS "Including 3Dconnexion SpaceMouse navigation support in pagelayout editor" ) + add_subdirectory( navlib ) + +-target_link_libraries( pl_editor_kiface pl_editor_navlib) ++set( PL_EDITOR_KIFACE_LIBRARIES ++ pl_editor_kiface_objects ++ pl_editor_navlib ++ kicommon ++ kiplatform ++ common ++ gal ++ core ++ ${wxWidgets_LIBRARIES} ++ ) ++ ++# WASM: Link kiface objects directly into pl_editor executable (static linking) ++if( EMSCRIPTEN ) ++ target_link_libraries( pl_editor ++ PRIVATE ++ ${PL_EDITOR_KIFACE_LIBRARIES} ++ ) ++ target_link_options( pl_editor PRIVATE ++ "LINKER:--allow-multiple-definition" ++ ) ++endif() ++ ++target_link_libraries( pl_editor_kiface ++ PRIVATE ++ ${PL_EDITOR_KIFACE_LIBRARIES} ++ ) ++ ++# if building pl_editor, then also build pl_editor_kiface if out of date. ++add_dependencies( pl_editor pl_editor_kiface ) + +-add_dependencies( pl_editor_kiface pl_editor_navlib) ++add_dependencies( pl_editor_kiface pl_editor_navlib ) + + # these 2 binaries are a matched set, keep them together: + if( APPLE ) +diff --git a/pagelayout_editor/navlib/CMakeLists.txt b/pagelayout_editor/navlib/CMakeLists.txt +index ad3c388736..89e94e6abb 100644 +--- a/pagelayout_editor/navlib/CMakeLists.txt ++++ b/pagelayout_editor/navlib/CMakeLists.txt +@@ -1,25 +1,36 @@ + +-add_library(pl_editor_navlib STATIC +- "nl_pl_editor_plugin.cpp" +- "nl_pl_editor_plugin_impl.cpp" +-) ++# WASM: 3D mouse support not available, use stubs ++if( EMSCRIPTEN ) ++ add_library(pl_editor_navlib STATIC ++ "${CMAKE_SOURCE_DIR}/../wasm/stubs/nl_pl_editor_plugin_stub.cpp" ++ ) ++ target_include_directories(pl_editor_navlib PRIVATE ++ ${CMAKE_SOURCE_DIR}/pagelayout_editor ++ ${CMAKE_SOURCE_DIR}/include ++ ) ++else() ++ add_library(pl_editor_navlib STATIC ++ "nl_pl_editor_plugin.cpp" ++ "nl_pl_editor_plugin_impl.cpp" ++ ) + +-# pl_editor_navlib depends on make_lexer outputs in common +-add_dependencies( pl_editor_navlib common ) ++ # pl_editor_navlib depends on make_lexer outputs in common ++ add_dependencies( pl_editor_navlib common ) + +-# Find the 3DxWare SDK component 3DxWare::NlClient +-# find_package(TDxWare_SDK 4.0 REQUIRED COMPONENTS 3DxWare::Navlib) +-target_compile_definitions(pl_editor_navlib PRIVATE +- $ +-) +-target_compile_options(pl_editor_navlib PRIVATE +- $ +-) +-target_include_directories(pl_editor_navlib PRIVATE +- $ +- $ +-) +-target_link_libraries(pl_editor_navlib +- $ +- 3DxWare::Navlib +-) ++ # Find the 3DxWare SDK component 3DxWare::NlClient ++ # find_package(TDxWare_SDK 4.0 REQUIRED COMPONENTS 3DxWare::Navlib) ++ target_compile_definitions(pl_editor_navlib PRIVATE ++ $ ++ ) ++ target_compile_options(pl_editor_navlib PRIVATE ++ $ ++ ) ++ target_include_directories(pl_editor_navlib PRIVATE ++ $ ++ $ ++ ) ++ target_link_libraries(pl_editor_navlib ++ $ ++ 3DxWare::Navlib ++ ) ++endif() diff --git a/features/pl-editor/root.patch b/features/pl-editor/root.patch new file mode 100644 index 0000000..e43cdf0 --- /dev/null +++ b/features/pl-editor/root.patch @@ -0,0 +1,621 @@ +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 [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 [options] + # + # Args: +-# pcbnew | eeschema | calculator (required) ++# 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 argument (pcbnew | eeschema | calculator)" >&2 ++ echo "Error: missing 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 @@ ++ ++ ++ ++ ++ ++ KiCad Page Layout Editor WASM ++ ++ ++ ++
++ ++
++
Initializing...
++
++
++ ++
++ ++ ++ ++ ++ ++ ++ ++ +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 { ++ 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 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 destructor needs a complete type. ++class NL_PL_EDITOR_PLUGIN_IMPL {}; ++ ++#include ++ ++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; ++} diff --git a/features/pl-editor/wxwidgets.patch b/features/pl-editor/wxwidgets.patch new file mode 100644 index 0000000..e12f985 --- /dev/null +++ b/features/pl-editor/wxwidgets.patch @@ -0,0 +1,75 @@ +diff --git a/src/generic/filedlgg.cpp b/src/generic/filedlgg.cpp +index 4b89dcdc6f4..e895557f86d 100644 +--- a/src/generic/filedlgg.cpp ++++ b/src/generic/filedlgg.cpp +@@ -337,6 +337,18 @@ void wxGenericFileDialog::OnOk( wxCommandEvent &WXUNUSED(event) ) + + const wxString& path = selectedFiles[0]; + ++ // If the user OKs a directory (via single-click + Enter/OK, or via a ++ // double-click that routed through here instead of wxGenericFileCtrl's ++ // OnActivated), navigate into the directory rather than closing the dialog ++ // and surfacing the folder path to the caller as if it were a file. ++ // Without this, KiCad's Open Drawing Sheet then tries to LoadDrawingSheetFile ++ // on the folder and surfaces "Unable to load /dev file" to the user. ++ if (selectedFiles.Count() == 1 && wxDirExists(path)) ++ { ++ m_filectrl->SetDirectory(path); ++ return; ++ } ++ + if (selectedFiles.Count() == 1) + { + SetPath(path); +diff --git a/src/wasm/mouse.cpp b/src/wasm/mouse.cpp +index e3a2d1cfdef..62fb7c92cba 100644 +--- a/src/wasm/mouse.cpp ++++ b/src/wasm/mouse.cpp +@@ -12,7 +12,9 @@ + #include "wx/log.h" + #include + +-//#define HAS_MOUSE_DETAIL ++// Double-click detection threshold (ms). Matches the default ++// wxSYS_DCLICK_MSEC on most platforms. ++#define WASM_DCLICK_MSEC 500.0 + + namespace + { +@@ -63,11 +65,32 @@ wxEventType GetMouseEventType(int emscriptenEventType, + wxEventType eventType; + std::string eventName; + +-#ifdef HAS_MOUSE_DETAIL +- int clickCount = event.detail; +-#else ++ // EmscriptenMouseEvent no longer exposes a click-count field, so we ++ // detect double-clicks ourselves: two MOUSEDOWNs of the same button ++ // within WASM_DCLICK_MSEC count as a double-click. The browser also ++ // dispatches a real 'dblclick' event we could hook, but tracking it ++ // on MOUSEDOWN lets wxEVT_LEFT_DCLICK arrive at the same point in the ++ // sequence as on desktop (between LEFT_DOWN and LEFT_UP), which is ++ // what wxGenericListCtrl's activation logic expects. ++ static double lastMouseDownTime = 0.0; ++ static unsigned short lastMouseDownButton = 0xFFFF; + int clickCount = 1; +-#endif ++ if (emscriptenEventType == EMSCRIPTEN_EVENT_MOUSEDOWN) ++ { ++ if (event.button == lastMouseDownButton && ++ (event.timestamp - lastMouseDownTime) < WASM_DCLICK_MSEC) ++ { ++ clickCount = 2; ++ // Reset so a quick third click isn't chained as another DCLICK. ++ lastMouseDownTime = 0.0; ++ lastMouseDownButton = 0xFFFF; ++ } ++ else ++ { ++ lastMouseDownTime = event.timestamp; ++ lastMouseDownButton = event.button; ++ } ++ } + + switch (emscriptenEventType) + { diff --git a/kicad b/kicad index 4cad41a..881ab17 160000 --- a/kicad +++ b/kicad @@ -1 +1 @@ -Subproject commit 4cad41af0665d166f9da4c69ae1e42d5a043538f +Subproject commit 881ab171814da6e73e116f91e9e18fb4dd9fcf6b 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 [options] # # Args: -# pcbnew | eeschema | calculator (required) +# 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 argument (pcbnew | eeschema | calculator)" >&2 + echo "Error: missing 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 @@ + + + + + + KiCad Page Layout Editor WASM + + + +
+ +
+
Initializing...
+
+
+ +
+ + + + + + + + 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 { + 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 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 destructor needs a complete type. +class NL_PL_EDITOR_PLUGIN_IMPL {}; + +#include + +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; +} diff --git a/wxwidgets b/wxwidgets index 6fb2eac..6583b43 160000 --- a/wxwidgets +++ b/wxwidgets @@ -1 +1 @@ -Subproject commit 6fb2eac2572cf0d3964ba8bec8d73e017d311733 +Subproject commit 6583b434479feb4e56cf21a09c117f447165894c