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>
This commit is contained in:
parent
eb15f72e8f
commit
d735779e23
14 changed files with 1436 additions and 11 deletions
|
|
@ -29,7 +29,7 @@ set -e
|
||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
VALID_APPS="pcbnew | eeschema | calculator | all"
|
VALID_APPS="pcbnew | eeschema | calculator | pl_editor | all"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
echo "Usage: ./docker/build.sh <app> [args...]" >&2
|
echo "Usage: ./docker/build.sh <app> [args...]" >&2
|
||||||
|
|
@ -52,7 +52,7 @@ APP_NAME="$1"
|
||||||
shift
|
shift
|
||||||
|
|
||||||
case "$APP_NAME" in
|
case "$APP_NAME" in
|
||||||
pcbnew|eeschema|calculator|all) ;;
|
pcbnew|eeschema|calculator|pl_editor|all) ;;
|
||||||
*)
|
*)
|
||||||
echo "Error: unknown app '$APP_NAME' (expected: ${VALID_APPS})" >&2
|
echo "Error: unknown app '$APP_NAME' (expected: ${VALID_APPS})" >&2
|
||||||
usage
|
usage
|
||||||
|
|
@ -115,6 +115,7 @@ fi
|
||||||
kicad_subdir_for() {
|
kicad_subdir_for() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
calculator) echo "pcb_calculator" ;;
|
calculator) echo "pcb_calculator" ;;
|
||||||
|
pl_editor) echo "pagelayout_editor" ;;
|
||||||
*) echo "$1" ;;
|
*) echo "$1" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
@ -158,6 +159,7 @@ if [[ "${APP_NAME}" == "all" ]]; then
|
||||||
build_app pcbnew
|
build_app pcbnew
|
||||||
build_app eeschema
|
build_app eeschema
|
||||||
build_app calculator
|
build_app calculator
|
||||||
|
build_app pl_editor
|
||||||
else
|
else
|
||||||
build_app "${APP_NAME}"
|
build_app "${APP_NAME}"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
54
features/pl-editor/0001-pl-editor-port.md
Normal file
54
features/pl-editor/0001-pl-editor-port.md
Normal file
|
|
@ -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).
|
||||||
194
features/pl-editor/kicad.patch
Normal file
194
features/pl-editor/kicad.patch
Normal file
|
|
@ -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
|
||||||
|
$<$<BOOL:${KICAD_MAKE_LINK_MAPS}>:-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
|
||||||
|
$<$<BOOL:${KICAD_MAKE_LINK_MAPS}>:-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_PROPERTY:3DxWare::Navlib,INTERFACE_COMPILE_DEFINITIONS>
|
||||||
|
-)
|
||||||
|
-target_compile_options(pl_editor_navlib PRIVATE
|
||||||
|
- $<TARGET_PROPERTY:3DxWare::Navlib,INTERFACE_COMPILE_OPTIONS>
|
||||||
|
-)
|
||||||
|
-target_include_directories(pl_editor_navlib PRIVATE
|
||||||
|
- $<TARGET_PROPERTY:3DxWare::Navlib,INTERFACE_INCLUDE_DIRECTORIES>
|
||||||
|
- $<TARGET_PROPERTY:pl_editor_kiface,INCLUDE_DIRECTORIES>
|
||||||
|
-)
|
||||||
|
-target_link_libraries(pl_editor_navlib
|
||||||
|
- $<TARGET_PROPERTY:3DxWare::Navlib,INTERFACE_LINK_LIBRARIES>
|
||||||
|
- 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_PROPERTY:3DxWare::Navlib,INTERFACE_COMPILE_DEFINITIONS>
|
||||||
|
+ )
|
||||||
|
+ target_compile_options(pl_editor_navlib PRIVATE
|
||||||
|
+ $<TARGET_PROPERTY:3DxWare::Navlib,INTERFACE_COMPILE_OPTIONS>
|
||||||
|
+ )
|
||||||
|
+ target_include_directories(pl_editor_navlib PRIVATE
|
||||||
|
+ $<TARGET_PROPERTY:3DxWare::Navlib,INTERFACE_INCLUDE_DIRECTORIES>
|
||||||
|
+ $<TARGET_PROPERTY:pl_editor_kiface,INCLUDE_DIRECTORIES>
|
||||||
|
+ )
|
||||||
|
+ target_link_libraries(pl_editor_navlib
|
||||||
|
+ $<TARGET_PROPERTY:3DxWare::Navlib,INTERFACE_LINK_LIBRARIES>
|
||||||
|
+ 3DxWare::Navlib
|
||||||
|
+ )
|
||||||
|
+endif()
|
||||||
621
features/pl-editor/root.patch
Normal file
621
features/pl-editor/root.patch
Normal file
|
|
@ -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 <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;
|
||||||
|
+}
|
||||||
75
features/pl-editor/wxwidgets.patch
Normal file
75
features/pl-editor/wxwidgets.patch
Normal file
|
|
@ -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 <emscripten/html5.h>
|
||||||
|
|
||||||
|
-//#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)
|
||||||
|
{
|
||||||
2
kicad
2
kicad
|
|
@ -1 +1 @@
|
||||||
Subproject commit 4cad41af0665d166f9da4c69ae1e42d5a043538f
|
Subproject commit 881ab171814da6e73e116f91e9e18fb4dd9fcf6b
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Build a KiCad app (pcbnew, eeschema, calculator) for WebAssembly.
|
# Build a KiCad app (pcbnew, eeschema, calculator, pl_editor) for WebAssembly.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./scripts/kicad/build-kicad-target.sh <app> [options]
|
# ./scripts/kicad/build-kicad-target.sh <app> [options]
|
||||||
#
|
#
|
||||||
# Args:
|
# Args:
|
||||||
# <app> pcbnew | eeschema | calculator (required)
|
# <app> pcbnew | eeschema | calculator | pl_editor (required)
|
||||||
#
|
#
|
||||||
# Options:
|
# Options:
|
||||||
# --full Full clean rebuild (dependencies + KiCad)
|
# --full Full clean rebuild (dependencies + KiCad)
|
||||||
|
|
@ -26,25 +26,26 @@
|
||||||
# the source subdirectory. Calculator is the exception: app=calculator but the
|
# the source subdirectory. Calculator is the exception: app=calculator but the
|
||||||
# upstream target and source subdir are both pcb_calculator (the OUTPUT_NAME
|
# upstream target and source subdir are both pcb_calculator (the OUTPUT_NAME
|
||||||
# property in pcb_calculator/CMakeLists.txt emits calculator.{js,wasm}).
|
# 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
|
set -e
|
||||||
|
|
||||||
if [ -z "$1" ]; then
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
APP_NAME="$1"
|
APP_NAME="$1"
|
||||||
shift
|
shift
|
||||||
|
|
||||||
case "$APP_NAME" in
|
case "$APP_NAME" in
|
||||||
pcbnew|eeschema)
|
pcbnew|eeschema|pl_editor)
|
||||||
KICAD_TARGET="$APP_NAME"
|
KICAD_TARGET="$APP_NAME"
|
||||||
;;
|
;;
|
||||||
calculator)
|
calculator)
|
||||||
KICAD_TARGET="pcb_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
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
|
||||||
7
scripts/kicad/build-pl_editor.sh
Executable file
7
scripts/kicad/build-pl_editor.sh
Executable file
|
|
@ -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 "$@"
|
||||||
200
tests/apps/kicad/pl_editor.html
Normal file
200
tests/apps/kicad/pl_editor.html
Normal file
|
|
@ -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>
|
||||||
37
tests/e2e/filedialog-folder-nav.spec.ts
Normal file
37
tests/e2e/filedialog-folder-nav.spec.ts
Normal file
|
|
@ -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);
|
||||||
|
});
|
||||||
202
tests/kicad/pl_editor.spec.ts
Normal file
202
tests/kicad/pl_editor.spec.ts
Normal file
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -17,10 +17,12 @@ mkdir -p "$KICAD_TEST"
|
||||||
|
|
||||||
# Map an app name to its inner CMake build subdirectory. Most apps share their
|
# 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
|
# 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() {
|
kicad_subdir_for() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
calculator) echo "pcb_calculator" ;;
|
calculator) echo "pcb_calculator" ;;
|
||||||
|
pl_editor) echo "pagelayout_editor" ;;
|
||||||
*) echo "$1" ;;
|
*) echo "$1" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
@ -64,9 +66,10 @@ found_any=0
|
||||||
copy_app pcbnew && found_any=1
|
copy_app pcbnew && found_any=1
|
||||||
copy_app eeschema && found_any=1
|
copy_app eeschema && found_any=1
|
||||||
copy_app calculator && found_any=1
|
copy_app calculator && found_any=1
|
||||||
|
copy_app pl_editor && found_any=1
|
||||||
|
|
||||||
if [ "$found_any" -eq 0 ]; then
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
29
wasm/stubs/nl_pl_editor_plugin_stub.cpp
Normal file
29
wasm/stubs/nl_pl_editor_plugin_stub.cpp
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 6fb2eac2572cf0d3964ba8bec8d73e017d311733
|
Subproject commit 6583b434479feb4e56cf21a09c117f447165894c
|
||||||
Loading…
Reference in a new issue