From 8e413f89ec183906d987a864186318d80276c94f Mon Sep 17 00:00:00 2001 From: Istvan Matejcsok <119620946+matejcsok-ee@users.noreply.github.com> Date: Fri, 5 Jun 2026 12:15:54 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20cleanup=20and=20or?= =?UTF-8?q?ganize=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + README.md | 18 +- docs/README.md | 58 + build.md => docs/build.md | 0 DEBUG.md => docs/debugging/DEBUG.md | 0 learning.md => docs/debugging/learning.md | 0 .../features/archive}/ipc-api/ipc-api.md | 2 + .../webgl/0001-opengl-gal-webgl-strategy.md | 2 + .../0002-gal-native-test-architecture.md | 2 + .../0003-webgl-gal-implementation-plan.md | 2 + ...icad-wasm-tool-activation-investigation.md | 0 .../0002-wasm-coroutine-deep-dive.md | 0 .../0003-wxauitoolbar-registration-fix.md | 2 +- .../rtree-debug-findings.md | 0 .../features}/gerbview/0001-gerbview-port.md | 0 .../pl-editor/0001-pl-editor-port.md | 0 .../schematic/0001-eeschema-iface-stubs.md | 0 .../symbol-editor/0001-symbol-editor-port.md | 2 +- .../features}/web-init/0001-web-app-spec.md | 0 .../web-init/0002-url-regex-modal-followup.md | 0 {research => docs/research}/threading_1.md | 0 {research => docs/research}/threading_2.md | 2 +- features/pl-editor/kicad.patch | 194 -- features/pl-editor/root.patch | 621 ----- features/pl-editor/wxwidgets.patch | 75 - features/schematic/kicad.patch | 432 ---- features/schematic/root.patch | 2005 ----------------- scripts/common/apply-asyncify.sh | 2 +- web/README.md | 2 +- 29 files changed, 85 insertions(+), 3340 deletions(-) create mode 100644 docs/README.md rename build.md => docs/build.md (100%) rename DEBUG.md => docs/debugging/DEBUG.md (100%) rename learning.md => docs/debugging/learning.md (100%) rename {features => docs/features/archive}/ipc-api/ipc-api.md (95%) rename {features => docs/features/archive}/webgl/0001-opengl-gal-webgl-strategy.md (96%) rename {features => docs/features/archive}/webgl/0002-gal-native-test-architecture.md (97%) rename {features => docs/features/archive}/webgl/0003-webgl-gal-implementation-plan.md (97%) rename {features => docs/features}/browser-tools/0001-kicad-wasm-tool-activation-investigation.md (100%) rename {features => docs/features}/browser-tools/0002-wasm-coroutine-deep-dive.md (100%) rename {features => docs/features}/browser-tools/0003-wxauitoolbar-registration-fix.md (98%) rename {features => docs/features}/fix-asyncify-O2-and-modal-promise-rejection/rtree-debug-findings.md (100%) rename {features => docs/features}/gerbview/0001-gerbview-port.md (100%) rename {features => docs/features}/pl-editor/0001-pl-editor-port.md (100%) rename {features => docs/features}/schematic/0001-eeschema-iface-stubs.md (100%) rename {features => docs/features}/symbol-editor/0001-symbol-editor-port.md (97%) rename {features => docs/features}/web-init/0001-web-app-spec.md (100%) rename {features => docs/features}/web-init/0002-url-regex-modal-followup.md (100%) rename {research => docs/research}/threading_1.md (100%) rename {research => docs/research}/threading_2.md (99%) delete mode 100644 features/pl-editor/kicad.patch delete mode 100644 features/pl-editor/root.patch delete mode 100644 features/pl-editor/wxwidgets.patch delete mode 100644 features/schematic/kicad.patch delete mode 100644 features/schematic/root.patch diff --git a/.gitignore b/.gitignore index abc99e5..53da527 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,7 @@ output/ .playwright-mcp .claude/worktrees/ +# Feature patch scratch (generated by scripts/create-feature-patches.sh). +# Curated design docs live under docs/features/ and ARE committed. +/features/ + diff --git a/README.md b/README.md index 18506ea..63f1f1c 100644 --- a/README.md +++ b/README.md @@ -64,13 +64,12 @@ kicad-wasm/ ## Feature Branches -Each feature branch has a folder in `features//` containing: -- Documentation and research notes -- `root.patch` - changes to main repo -- `kicad.patch` - changes to KiCad fork -- `wxwidgets.patch` - changes to wxWidgets fork +Curated design docs and research notes for each feature live in +[`docs/features//`](docs/features/) (committed). -Generate patches: `./scripts/create-feature-patches.sh [branch-name]` +`./scripts/create-feature-patches.sh [branch-name]` generates per-branch patches +(`root.patch`, `kicad.patch`, `wxwidgets.patch`) into a local `features//` +scratch dir. That dir is gitignored — the patches are local history, not committed. ## Two Build Workflows @@ -91,7 +90,7 @@ cd tests && npm install && npm run test:kicad Output: `output/pcbnew.js`, `output/pcbnew.wasm` -See [build.md](build.md) for detailed build documentation. +See [docs/build.md](docs/build.md) for detailed build documentation. ### 2. wxWidgets Test Apps (Local) @@ -152,8 +151,11 @@ See [tests/README.md](tests/README.md) for test documentation. ## Documentation -- [Build System](build.md) - Docker build details +See **[docs/README.md](docs/README.md)** for the full documentation map. Highlights: + +- [Build System](docs/build.md) - Docker build details - [Docker README](docker/README.md) - Container setup +- [Debugging Guide](docs/debugging/DEBUG.md) - Asyncify/WASM debugging - [Tests README](tests/README.md) - Test infrastructure ## License diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..0882500 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,58 @@ +# Documentation Map + +A central index of the documentation in this repo. The goal of the project is to +build KiCad with WASM and run it in a browser. + +> Note: per-area `README.md` files stay next to the code they describe (they're linked +> below). Cross-cutting guides live under `docs/`. Per-feature design notes live under +> [`features/`](features/). + +## Start here + +- [Project README](../README.md) — overview, prerequisites, quick start, project structure +- [CLAUDE.md](../CLAUDE.md) — project/agent context and contribution conventions + +## Build + +- [docs/build.md](build.md) — Docker-based KiCad WASM build system (two-phase build, outputs, memory) +- [docker/README.md](../docker/README.md) — Docker build environment, branch-specific containers, troubleshooting +- [wasm/README.md](../wasm/README.md) — WASM compatibility layer (overrides/shims without patching KiCad) + +## Debugging & Asyncify + +- [docs/debugging/DEBUG.md](debugging/DEBUG.md) — debugging guide: Asyncify stalls vs crashes, shim/codegen coupling, stub-bisection +- [docs/debugging/learning.md](debugging/learning.md) — Asyncify + consecutive modal dialogs: the lock pattern +- [docs/research/threading_1.md](research/threading_1.md) — deep dive: the Asyncify single-slot `currData` collision bug and the fix +- [docs/research/threading_2.md](research/threading_2.md) — external research: JSPI/WasmFX/state-machine alternatives, QEMU analysis + +## Architecture + +- [wasm/README.md](../wasm/README.md) — WASM compatibility layer structure +- [web/README.md](../web/README.md) — web app (create/open KiCad projects), tech stack, URL routing, WASM artifact serving + +## Testing + +- [tests/README.md](../tests/README.md) — Playwright test infrastructure, element registry, logs, screenshots +- [tests/WHATWORKS.md](../tests/WHATWORKS.md) — wxWidgets-in-WASM feature coverage matrix and KiCad readiness +- [tests/GL_README.md](../tests/GL_README.md) — Emscripten legacy GL immediate-mode quirks (color-per-vertex) +- [tests/gal-regression/README.md](../tests/gal-regression/README.md) — GAL visual regression suite (native OpenGL vs WebGL WASM) + +## Feature design docs + +Per-feature design notes and porting records live under [`features/`](features/): + +- [web-init](features/web-init/) — web app spec +- [schematic](features/schematic/) — eeschema WASM bring-up +- [symbol-editor](features/symbol-editor/) — symbol editor (eeschema kiface launcher) +- [gerbview](features/gerbview/) — Gerber viewer port +- [pl-editor](features/pl-editor/) — page-layout editor port (incl. file-dialog usability fixes) +- [browser-tools](features/browser-tools/) — tool-activation / coroutine deep dives +- [fix-asyncify-O2-and-modal-promise-rejection](features/fix-asyncify-O2-and-modal-promise-rejection/) — RTree wasm overflow bug investigation + +### Archived / historical + +[`features/archive/`](features/archive/) holds docs whose work is done or superseded +(each carries a status banner): + +- [webgl](features/archive/webgl/) — WebGL-GAL strategy/plan (since implemented in `kicad/common/gal/webgl/`) +- [ipc-api](features/archive/ipc-api/) — IPC-API guard cleanup TODO (revert not yet actioned) diff --git a/build.md b/docs/build.md similarity index 100% rename from build.md rename to docs/build.md diff --git a/DEBUG.md b/docs/debugging/DEBUG.md similarity index 100% rename from DEBUG.md rename to docs/debugging/DEBUG.md diff --git a/learning.md b/docs/debugging/learning.md similarity index 100% rename from learning.md rename to docs/debugging/learning.md diff --git a/features/ipc-api/ipc-api.md b/docs/features/archive/ipc-api/ipc-api.md similarity index 95% rename from features/ipc-api/ipc-api.md rename to docs/features/archive/ipc-api/ipc-api.md index a6a9692..529b4cc 100644 --- a/features/ipc-api/ipc-api.md +++ b/docs/features/archive/ipc-api/ipc-api.md @@ -1,5 +1,7 @@ # KiCad IPC API - WASM Fork Changes +> **ARCHIVED — open cleanup TODO, not yet actioned.** Describes `#ifdef KICAD_IPC_API` guards added to ~18 KiCad source files that could be reverted to reduce fork diff. Archiving this doc does **not** perform the revert — that remains a separate task. + ## Overview We added `#ifdef KICAD_IPC_API` guards to 18 KiCad source files. These guards wrap: diff --git a/features/webgl/0001-opengl-gal-webgl-strategy.md b/docs/features/archive/webgl/0001-opengl-gal-webgl-strategy.md similarity index 96% rename from features/webgl/0001-opengl-gal-webgl-strategy.md rename to docs/features/archive/webgl/0001-opengl-gal-webgl-strategy.md index eda1be3..358cfdb 100644 --- a/features/webgl/0001-opengl-gal-webgl-strategy.md +++ b/docs/features/archive/webgl/0001-opengl-gal-webgl-strategy.md @@ -1,5 +1,7 @@ # OpenGL, GAL, and WebGL Strategy for KiCad WASM +> **ARCHIVED / HISTORICAL** — the WebGL-GAL backend described here was implemented (see `kicad/common/gal/webgl/`, ~22.5k lines). Live test docs: [`tests/gal-regression/README.md`](../../../../tests/gal-regression/README.md). Kept for design rationale. + ## Summary This document analyzes KiCad's graphics architecture and evaluates strategies for WebGL rendering in the WASM build. diff --git a/features/webgl/0002-gal-native-test-architecture.md b/docs/features/archive/webgl/0002-gal-native-test-architecture.md similarity index 97% rename from features/webgl/0002-gal-native-test-architecture.md rename to docs/features/archive/webgl/0002-gal-native-test-architecture.md index 1cd06e8..5390c17 100644 --- a/features/webgl/0002-gal-native-test-architecture.md +++ b/docs/features/archive/webgl/0002-gal-native-test-architecture.md @@ -1,5 +1,7 @@ # GAL Native Test Harness Architecture +> **ARCHIVED / HISTORICAL** — the WebGL-GAL backend described here was implemented (see `kicad/common/gal/webgl/`). Live test docs: [`tests/gal-regression/README.md`](../../../../tests/gal-regression/README.md). Kept for design rationale. + ## Overview The GAL native test harness is a standalone macOS application that compiles KiCad's actual `OPENGL_GAL` rendering engine against system wxWidgets. It generates baseline PNG screenshots for visual regression testing of WebGL rendering in the WASM build. diff --git a/features/webgl/0003-webgl-gal-implementation-plan.md b/docs/features/archive/webgl/0003-webgl-gal-implementation-plan.md similarity index 97% rename from features/webgl/0003-webgl-gal-implementation-plan.md rename to docs/features/archive/webgl/0003-webgl-gal-implementation-plan.md index 95ecf70..ccc9d66 100644 --- a/features/webgl/0003-webgl-gal-implementation-plan.md +++ b/docs/features/archive/webgl/0003-webgl-gal-implementation-plan.md @@ -1,5 +1,7 @@ # WebGL GAL Port - Implementation Complete +> **ARCHIVED / HISTORICAL** — the WebGL-GAL backend described here was implemented (see `kicad/common/gal/webgl/`). Live test docs: [`tests/gal-regression/README.md`](../../../../tests/gal-regression/README.md). Kept for design rationale. + ## Status: ALL PHASES COMPLETE ✅ **Branch:** `webgl` (21+ commits) diff --git a/features/browser-tools/0001-kicad-wasm-tool-activation-investigation.md b/docs/features/browser-tools/0001-kicad-wasm-tool-activation-investigation.md similarity index 100% rename from features/browser-tools/0001-kicad-wasm-tool-activation-investigation.md rename to docs/features/browser-tools/0001-kicad-wasm-tool-activation-investigation.md diff --git a/features/browser-tools/0002-wasm-coroutine-deep-dive.md b/docs/features/browser-tools/0002-wasm-coroutine-deep-dive.md similarity index 100% rename from features/browser-tools/0002-wasm-coroutine-deep-dive.md rename to docs/features/browser-tools/0002-wasm-coroutine-deep-dive.md diff --git a/features/browser-tools/0003-wxauitoolbar-registration-fix.md b/docs/features/browser-tools/0003-wxauitoolbar-registration-fix.md similarity index 98% rename from features/browser-tools/0003-wxauitoolbar-registration-fix.md rename to docs/features/browser-tools/0003-wxauitoolbar-registration-fix.md index 623147a..c66cad7 100644 --- a/features/browser-tools/0003-wxauitoolbar-registration-fix.md +++ b/docs/features/browser-tools/0003-wxauitoolbar-registration-fix.md @@ -2,7 +2,7 @@ ## Context -The nested Asyncify collision bug (see `0002-wasm-coroutine-deep-dive.md` and `research/threading_1.md`) is fixed. KiCad WASM now loads through the startup wizard without crashing, and the full PCBnew UI renders — menus, left drawing-tool sidebar with Line/Circle/Rectangle icons, layer panel, PCB canvas — all visible. +The nested Asyncify collision bug (see `0002-wasm-coroutine-deep-dive.md` and `../../research/threading_1.md`) is fixed. KiCad WASM now loads through the startup wizard without crashing, and the full PCBnew UI renders — menus, left drawing-tool sidebar with Line/Circle/Rectangle icons, layer panel, PCB canvas — all visible. But tools still don't work end-to-end: diff --git a/features/fix-asyncify-O2-and-modal-promise-rejection/rtree-debug-findings.md b/docs/features/fix-asyncify-O2-and-modal-promise-rejection/rtree-debug-findings.md similarity index 100% rename from features/fix-asyncify-O2-and-modal-promise-rejection/rtree-debug-findings.md rename to docs/features/fix-asyncify-O2-and-modal-promise-rejection/rtree-debug-findings.md diff --git a/features/gerbview/0001-gerbview-port.md b/docs/features/gerbview/0001-gerbview-port.md similarity index 100% rename from features/gerbview/0001-gerbview-port.md rename to docs/features/gerbview/0001-gerbview-port.md diff --git a/features/pl-editor/0001-pl-editor-port.md b/docs/features/pl-editor/0001-pl-editor-port.md similarity index 100% rename from features/pl-editor/0001-pl-editor-port.md rename to docs/features/pl-editor/0001-pl-editor-port.md diff --git a/features/schematic/0001-eeschema-iface-stubs.md b/docs/features/schematic/0001-eeschema-iface-stubs.md similarity index 100% rename from features/schematic/0001-eeschema-iface-stubs.md rename to docs/features/schematic/0001-eeschema-iface-stubs.md diff --git a/features/symbol-editor/0001-symbol-editor-port.md b/docs/features/symbol-editor/0001-symbol-editor-port.md similarity index 97% rename from features/symbol-editor/0001-symbol-editor-port.md rename to docs/features/symbol-editor/0001-symbol-editor-port.md index 577948c..9a6b0fc 100644 --- a/features/symbol-editor/0001-symbol-editor-port.md +++ b/docs/features/symbol-editor/0001-symbol-editor-port.md @@ -34,7 +34,7 @@ verbatim. mirroring the eeschema/pcbnew static-link pattern. - **`kicad/eeschema/eeschema.cpp`** — `IFACE::CreateKiWindow`'s `FRAME_SCH_SYMBOL_EDITOR` case was stubbed to `return nullptr` on `__EMSCRIPTEN__` during the eeschema MVP - (see `features/schematic/0001-eeschema-iface-stubs.md`). That stub is now removed so + (see `../schematic/0001-eeschema-iface-stubs.md`). That stub is now removed so the frame is constructed in WASM like the native build. This was THE blocker: with the stub, `Kiway.Player(FRAME_SCH_SYMBOL_EDITOR)` returned null, `single_top`'s `OnInit` bailed, and the app sat idle with a blank canvas (no abort, no error). diff --git a/features/web-init/0001-web-app-spec.md b/docs/features/web-init/0001-web-app-spec.md similarity index 100% rename from features/web-init/0001-web-app-spec.md rename to docs/features/web-init/0001-web-app-spec.md diff --git a/features/web-init/0002-url-regex-modal-followup.md b/docs/features/web-init/0002-url-regex-modal-followup.md similarity index 100% rename from features/web-init/0002-url-regex-modal-followup.md rename to docs/features/web-init/0002-url-regex-modal-followup.md diff --git a/research/threading_1.md b/docs/research/threading_1.md similarity index 100% rename from research/threading_1.md rename to docs/research/threading_1.md diff --git a/research/threading_2.md b/docs/research/threading_2.md similarity index 99% rename from research/threading_2.md rename to docs/research/threading_2.md index 6404ddc..32b84cf 100644 --- a/research/threading_2.md +++ b/docs/research/threading_2.md @@ -148,7 +148,7 @@ The `emscripten_fiber_swap` mechanism handles this correctly because the fiber J ### The `setTimeout(wakeUp, 0)` Pattern -When Asyncify.wakeUp() is called while compiled code is still on the JS call stack, it corrupts state. The fix is always to defer: `setTimeout(wakeUp, 0)` ensures the previous operation has fully unwound before starting the next rewind. Our modal dialog code uses `setTimeout(0)` twice (double-deferred) for this reason — documented in `learning.md`. +When Asyncify.wakeUp() is called while compiled code is still on the JS call stack, it corrupts state. The fix is always to defer: `setTimeout(wakeUp, 0)` ensures the previous operation has fully unwound before starting the next rewind. Our modal dialog code uses `setTimeout(0)` twice (double-deferred) for this reason — documented in `../debugging/learning.md`. **Sources**: [emscripten #16291](https://github.com/emscripten-core/emscripten/issues/16291), [emscripten #18412](https://github.com/emscripten-core/emscripten/issues/18412), [emscripten #10515](https://github.com/emscripten-core/emscripten/issues/10515) diff --git a/features/pl-editor/kicad.patch b/features/pl-editor/kicad.patch deleted file mode 100644 index b0b158f..0000000 --- a/features/pl-editor/kicad.patch +++ /dev/null @@ -1,194 +0,0 @@ -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 deleted file mode 100644 index e43cdf0..0000000 --- a/features/pl-editor/root.patch +++ /dev/null @@ -1,621 +0,0 @@ -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 deleted file mode 100644 index e12f985..0000000 --- a/features/pl-editor/wxwidgets.patch +++ /dev/null @@ -1,75 +0,0 @@ -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/features/schematic/kicad.patch b/features/schematic/kicad.patch deleted file mode 100644 index 1346101..0000000 --- a/features/schematic/kicad.patch +++ /dev/null @@ -1,432 +0,0 @@ -diff --git a/eeschema/CMakeLists.txt b/eeschema/CMakeLists.txt -index 1be2c291d1..e07d05e6e7 100644 ---- a/eeschema/CMakeLists.txt -+++ b/eeschema/CMakeLists.txt -@@ -60,35 +60,48 @@ set( EESCHEMA_SCH_IO - - # HTTP IO plugin - sch_io/http_lib/sch_io_http_lib.cpp -+ ) - -- # Database IO plugin -- sch_io/database/sch_io_database.cpp -- -- # Eagle IO plugin -- sch_io/eagle/sch_io_eagle.cpp -- -- # Altium IO plugin -- sch_io/altium/altium_parser_sch.cpp -- sch_io/altium/sch_io_altium.cpp -- -- # Cadstar IO plugin -- sch_io/cadstar/cadstar_sch_archive_loader.cpp -- sch_io/cadstar/cadstar_sch_archive_parser.cpp -- sch_io/cadstar/sch_io_cadstar_archive.cpp -- -- # LTSpice IO plugin -- sch_io/ltspice/ltspice_schematic.cpp -- sch_io/ltspice/sch_io_ltspice.cpp -- sch_io/ltspice/sch_io_ltspice_parser.cpp -- -- # EasyEDA IO plugin -- sch_io/easyeda/sch_easyeda_parser.cpp -- sch_io/easyeda/sch_io_easyeda.cpp -- -- # EasyEDA Pro IO plugin -- sch_io/easyedapro/sch_easyedapro_parser.cpp -- sch_io/easyedapro/sch_io_easyedapro.cpp -+# Third-party importers and database plugin are not built for WASM: -+# - Database needs nanodbc/ODBC (common/database/database_connection.cpp -+# is also gated on NOT EMSCRIPTEN in common/CMakeLists.txt). -+# - Altium pulls in pcbnew/pcb_io/altium common code which pcbnew itself -+# excludes for WASM (third-party uses std::basic_string -+# that the current Emscripten libc++ rejects). -+# - Eagle/Cadstar/LTspice/EasyEDA are cross-tool importers we don't need -+# for the MVP and they bring transitive incompatible dependencies in. -+# sch_io_mgr.cpp's factory cases are guarded with __EMSCRIPTEN__ to match. -+if( NOT EMSCRIPTEN ) -+ list( APPEND EESCHEMA_SCH_IO -+ # Database IO plugin -+ sch_io/database/sch_io_database.cpp -+ -+ # Eagle IO plugin -+ sch_io/eagle/sch_io_eagle.cpp -+ -+ # Altium IO plugin -+ sch_io/altium/altium_parser_sch.cpp -+ sch_io/altium/sch_io_altium.cpp -+ -+ # Cadstar IO plugin -+ sch_io/cadstar/cadstar_sch_archive_loader.cpp -+ sch_io/cadstar/cadstar_sch_archive_parser.cpp -+ sch_io/cadstar/sch_io_cadstar_archive.cpp -+ -+ # LTSpice IO plugin -+ sch_io/ltspice/ltspice_schematic.cpp -+ sch_io/ltspice/sch_io_ltspice.cpp -+ sch_io/ltspice/sch_io_ltspice_parser.cpp -+ -+ # EasyEDA IO plugin -+ sch_io/easyeda/sch_easyeda_parser.cpp -+ sch_io/easyeda/sch_io_easyeda.cpp -+ -+ # EasyEDA Pro IO plugin -+ sch_io/easyedapro/sch_easyedapro_parser.cpp -+ sch_io/easyedapro/sch_io_easyedapro.cpp - ) -+endif() - - set( EESCHEMA_DLGS - dialogs/dialog_annotate.cpp -@@ -276,6 +289,22 @@ set( EESCHEMA_SIM_SRCS - widgets/tuner_slider_base.cpp - ) - -+# WASM: the four largest BSIM/SOI/HSIM data initializer functions exceed the -+# V8/SpiderMonkey limit on locals per function ("too many locals") once -+# compiled. Replace them with empty stubs — the simulator UI is unreachable -+# in WASM anyway (see wasm/stubs/ngspice/sharedspice.h). -+if( EMSCRIPTEN ) -+ list( REMOVE_ITEM EESCHEMA_SIM_SRCS -+ sim/sim_model_ngspice_data_bsim4.cpp -+ sim/sim_model_ngspice_data_b3soi.cpp -+ sim/sim_model_ngspice_data_b4soi.cpp -+ sim/sim_model_ngspice_data_hsim.cpp -+ ) -+ list( APPEND EESCHEMA_SIM_SRCS -+ ${CMAKE_SOURCE_DIR}/../wasm/stubs/eeschema_ngspice_data_stubs.cpp -+ ) -+endif() -+ - set( EESCHEMA_WIDGETS - widgets/hierarchy_pane.cpp - widgets/panel_sch_selection_filter_base.cpp -@@ -561,14 +590,25 @@ add_executable( eeschema WIN32 MACOSX_BUNDLE - ${EESCHEMA_RESOURCES} - ) - --set_source_files_properties( ${CMAKE_SOURCE_DIR}/common/single_top.cpp PROPERTIES -- COMPILE_DEFINITIONS "TOP_FRAME=FRAME_SCH;PGM_DATA_FILE_EXT=\"kicad_sch\";BUILD_KIWAY_DLL" -- ) -+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_SCH;PGM_DATA_FILE_EXT=\"kicad_sch\"" -+ ) -+else() -+ set_source_files_properties( ${CMAKE_SOURCE_DIR}/common/single_top.cpp PROPERTIES -+ COMPILE_DEFINITIONS "TOP_FRAME=FRAME_SCH;PGM_DATA_FILE_EXT=\"kicad_sch\";BUILD_KIWAY_DLL" -+ ) -+endif() - --target_link_libraries( eeschema -- kicommon -- ${wxWidgets_LIBRARIES} -- ) -+if( NOT EMSCRIPTEN ) -+ # Native: minimal link, kiface loaded dynamically -+ target_link_libraries( eeschema -+ kicommon -+ ${wxWidgets_LIBRARIES} -+ ) -+endif() -+# WASM linking is done after EESCHEMA_KIFACE_LIBRARIES is defined - - # the main Eeschema program, in DSO form. - add_library( eeschema_kiface_objects OBJECT -@@ -577,6 +617,15 @@ add_library( eeschema_kiface_objects OBJECT - ${EESCHEMA_COMMON_SRCS} - ) - -+# WASM: Bring eeschema.cpp (the KIFACE entry point) and stub sources into the -+# kiface_objects translation unit so static linking finds KIFACE_GETTER. -+if( EMSCRIPTEN ) -+ target_sources( eeschema_kiface_objects PRIVATE -+ eeschema.cpp -+ ${CMAKE_SOURCE_DIR}/../wasm/stubs/eeschema_frame_stub.cpp -+ ) -+endif() -+ - if( KICAD_USE_PCH ) - target_precompile_headers( eeschema_kiface_objects - PRIVATE -@@ -612,21 +661,49 @@ target_link_libraries( eeschema_kiface_objects PUBLIC eeschema_navlib) - - add_dependencies( eeschema_kiface_objects eeschema_navlib ) - --add_library( eeschema_kiface MODULE -- eeschema.cpp -+if( EMSCRIPTEN ) -+ # WASM: empty MODULE - eeschema.cpp was hoisted into kiface_objects above -+ # so the executable can link KIFACE_GETTER statically. The empty MODULE -+ # target is kept so post-build steps that reference it still resolve. -+ add_library( eeschema_kiface MODULE ) -+else() -+ add_library( eeschema_kiface MODULE -+ eeschema.cpp -+ ) -+endif() -+ -+set( EESCHEMA_KIFACE_LIBRARIES -+ eeschema_kiface_objects -+ common -+ eeschema_navlib -+ kicommon -+ kiplatform -+ gal -+ scripting -+ sexpr -+ core -+ markdown_lib -+ ${wxWidgets_LIBRARIES} -+ ${NGSPICE_LIBRARY} -+ Boost::headers - ) - -+# WASM: Link kiface objects directly into eeschema executable (static linking) -+if( EMSCRIPTEN ) -+ target_link_libraries( eeschema -+ PRIVATE -+ ${EESCHEMA_KIFACE_LIBRARIES} -+ ) -+ # WASM: Allow multiple definitions of symbols that exist in both -+ # KiCad and wxWidgets (matches the pcbnew WASM pattern). -+ target_link_options( eeschema PRIVATE -+ "LINKER:--allow-multiple-definition" -+ ) -+endif() -+ - target_link_libraries( eeschema_kiface - PRIVATE -- common -- eeschema_kiface_objects -- markdown_lib -- scripting -- sexpr -- core -- Boost::headers -- ${wxWidgets_LIBRARIES} -- ${NGSPICE_LIBRARY} -+ ${EESCHEMA_KIFACE_LIBRARIES} - ) - - if( MSVC ) -@@ -649,9 +726,17 @@ set_target_properties( eeschema_kiface PROPERTIES - ) - - # The KIFACE is in eeschema.cpp, export it: --set_source_files_properties( eeschema.cpp PROPERTIES -- COMPILE_DEFINITIONS "BUILD_KIWAY_DLL;COMPILING_DLL" -- ) -+if( EMSCRIPTEN ) -+ # WASM: Static linking - don't define BUILD_KIWAY_DLL so KIFACE_GETTER -+ # is exported with its statically-linkable name (KIFACE_1). -+ set_source_files_properties( eeschema.cpp PROPERTIES -+ COMPILE_DEFINITIONS "COMPILING_DLL" -+ ) -+else() -+ set_source_files_properties( eeschema.cpp PROPERTIES -+ COMPILE_DEFINITIONS "BUILD_KIWAY_DLL;COMPILING_DLL" -+ ) -+endif() - - # if building eeschema, then also build eeschema_kiface if out of date. - add_dependencies( eeschema eeschema_kiface ) -diff --git a/eeschema/dialogs/dialog_sim_command.cpp b/eeschema/dialogs/dialog_sim_command.cpp -index 7ec105f35a..19fa35d460 100644 ---- a/eeschema/dialogs/dialog_sim_command.cpp -+++ b/eeschema/dialogs/dialog_sim_command.cpp -@@ -570,7 +570,7 @@ void DIALOG_SIM_COMMAND::updateDCSources( wxChar aType, wxChoice* aSource ) - { - wxString prevSelection; - -- if( !aSource->IsEmpty() && aSource->GetSelection() >= 0 ) -+ if( aSource->GetCount() > 0 && aSource->GetSelection() >= 0 ) - prevSelection = aSource->GetString( aSource->GetSelection() ); - - std::set sourcesList; -diff --git a/eeschema/eeschema.cpp b/eeschema/eeschema.cpp -index f3b334d1c8..d56ca181dc 100644 ---- a/eeschema/eeschema.cpp -+++ b/eeschema/eeschema.cpp -@@ -201,7 +201,13 @@ static struct IFACE : public KIFACE_BASE, public UNITS_PROVIDER - } - - case FRAME_SCH_SYMBOL_EDITOR: -+#ifdef __EMSCRIPTEN__ -+ // WASM build: symbol library editor is not supported (see -+ // features/schematic/0001-eeschema-iface-stubs.md). -+ return nullptr; -+#else - return new SYMBOL_EDIT_FRAME( aKiway, aParent ); -+#endif - - case FRAME_SIMULATOR: - { -@@ -219,10 +225,19 @@ static struct IFACE : public KIFACE_BASE, public UNITS_PROVIDER - } - - case FRAME_SCH_VIEWER: -+#ifdef __EMSCRIPTEN__ -+ // WASM build: symbol viewer is not supported. -+ return nullptr; -+#else - return new SYMBOL_VIEWER_FRAME( aKiway, aParent ); -+#endif - - case FRAME_SYMBOL_CHOOSER: - { -+#ifdef __EMSCRIPTEN__ -+ // WASM build: symbol chooser is not supported (no bundled libs). -+ return nullptr; -+#else - bool cancelled = false; - SYMBOL_CHOOSER_FRAME* chooser = new SYMBOL_CHOOSER_FRAME( aKiway, aParent, cancelled ); - -@@ -233,6 +248,7 @@ static struct IFACE : public KIFACE_BASE, public UNITS_PROVIDER - } - - return chooser; -+#endif - } - - case DIALOG_SCH_LIBRARY_TABLE: -diff --git a/eeschema/sch_base_frame.cpp b/eeschema/sch_base_frame.cpp -index 161379a699..e722b7a043 100644 ---- a/eeschema/sch_base_frame.cpp -+++ b/eeschema/sch_base_frame.cpp -@@ -775,6 +775,7 @@ wxString SCH_BASE_FRAME::SelectLibrary( const wxString& aDialogTitle, const wxSt - - void SCH_BASE_FRAME::setSymWatcher( const LIB_ID* aID ) - { -+#if wxUSE_FSWATCHER - Unbind( wxEVT_FSWATCHER, &SCH_BASE_FRAME::OnSymChange, this ); - - if( m_watcher ) -@@ -824,11 +825,15 @@ void SCH_BASE_FRAME::setSymWatcher( const LIB_ID* aID ) - wxLogNull silence; - m_watcher->Add( fn ); - } -+#else -+ (void) aID; -+#endif - } - - - void SCH_BASE_FRAME::OnSymChange( wxFileSystemWatcherEvent& aEvent ) - { -+#if wxUSE_FSWATCHER - LEGACY_SYMBOL_LIBS* libs = PROJECT_SCH::LegacySchLibs( &Prj() ); - - wxLogTrace( traceLibWatch, "OnSymChange: %s, watcher file: %s", -@@ -846,6 +851,9 @@ void SCH_BASE_FRAME::OnSymChange( wxFileSystemWatcherEvent& aEvent ) - wxLogTrace( traceLibWatch, "Failed to start the debounce timer" ); - return; - } -+#else -+ (void) aEvent; -+#endif - } - - -diff --git a/eeschema/sch_base_frame.h b/eeschema/sch_base_frame.h -index 2d701c7d28..a86ae37237 100644 ---- a/eeschema/sch_base_frame.h -+++ b/eeschema/sch_base_frame.h -@@ -323,7 +323,9 @@ protected: - private: - - /// These are file watchers for the symbol library tables. -+#if wxUSE_FSWATCHER - std::unique_ptr m_watcher; -+#endif - wxFileName m_watcherFileName; - wxDateTime m_watcherLastModified; - wxTimer m_watcherDebounceTimer; -diff --git a/eeschema/sch_edit_frame.cpp b/eeschema/sch_edit_frame.cpp -index fd5a91a388..6eab185446 100644 ---- a/eeschema/sch_edit_frame.cpp -+++ b/eeschema/sch_edit_frame.cpp -@@ -56,7 +56,9 @@ - #include - #include - #include -+#ifdef KICAD_SCRIPTING - #include -+#endif - #include - #include - #include -diff --git a/eeschema/sch_io/sch_io_mgr.cpp b/eeschema/sch_io/sch_io_mgr.cpp -index 63e55bcdae..922fef0d90 100644 ---- a/eeschema/sch_io/sch_io_mgr.cpp -+++ b/eeschema/sch_io/sch_io_mgr.cpp -@@ -24,17 +24,22 @@ - #include - - #include --#include - #include - #include -+#include - -+// Third-party importers and the database plugin are excluded from the WASM -+// build (see kicad/eeschema/CMakeLists.txt). Their FindPlugin cases below -+// return nullptr on WASM. -+#ifndef __EMSCRIPTEN__ -+#include - #include - #include - #include - #include - #include - #include --#include -+#endif - #include // for ExpandEnvVarSubstitutions - - #include -@@ -68,6 +73,8 @@ SCH_IO* SCH_IO_MGR::FindPlugin( SCH_FILE_T aFileType ) - { - case SCH_KICAD: return new SCH_IO_KICAD_SEXPR(); - case SCH_LEGACY: return new SCH_IO_KICAD_LEGACY(); -+ case SCH_HTTP: return new SCH_IO_HTTP_LIB(); -+#ifndef __EMSCRIPTEN__ - case SCH_ALTIUM: return new SCH_IO_ALTIUM(); - case SCH_CADSTAR_ARCHIVE: return new SCH_IO_CADSTAR_ARCHIVE(); - case SCH_DATABASE: return new SCH_IO_DATABASE(); -@@ -75,7 +82,7 @@ SCH_IO* SCH_IO_MGR::FindPlugin( SCH_FILE_T aFileType ) - case SCH_EASYEDA: return new SCH_IO_EASYEDA(); - case SCH_EASYEDAPRO: return new SCH_IO_EASYEDAPRO(); - case SCH_LTSPICE: return new SCH_IO_LTSPICE(); -- case SCH_HTTP: return new SCH_IO_HTTP_LIB(); -+#endif - default: return nullptr; - } - } -diff --git a/eeschema/toolbars_sch_editor.cpp b/eeschema/toolbars_sch_editor.cpp -index 600f4379bc..a4df373475 100644 ---- a/eeschema/toolbars_sch_editor.cpp -+++ b/eeschema/toolbars_sch_editor.cpp -@@ -32,7 +32,9 @@ - #include - #include - #include -+#ifdef KICAD_SCRIPTING - #include -+#endif - #include - #include - #include -@@ -251,7 +253,11 @@ void SCH_EDIT_FRAME::configureToolbars() - [this]( ACTION_TOOLBAR* aToolbar ) - { - // Add scripting console and API plugins -+ #ifdef KICAD_SCRIPTING - bool scriptingAvailable = SCRIPTING::IsWxAvailable(); -+ #else -+ bool scriptingAvailable = false; -+ #endif - - #ifdef KICAD_IPC_API - bool haveApiPlugins = Pgm().GetCommonSettings()->m_Api.enable_server && diff --git a/features/schematic/root.patch b/features/schematic/root.patch deleted file mode 100644 index 56584c3..0000000 --- a/features/schematic/root.patch +++ /dev/null @@ -1,2005 +0,0 @@ -diff --git a/docker/build.sh b/docker/build.sh -index 290112a..11104be 100755 ---- a/docker/build.sh -+++ b/docker/build.sh -@@ -1,8 +1,13 @@ - #!/bin/bash --# Build KiCad WASM inside Docker container, then apply asyncify on host -- --# Redirect all output to a log file (re-execs script with redirection) --source "$(dirname "$0")/../scripts/common/logging.sh" -+# Build a KiCad editor (pcbnew or eeschema) inside Docker, then run asyncify -+# and friends on the host. -+# -+# Usage: -+# ./docker/build.sh # builds pcbnew (default) -+# ./docker/build.sh pcbnew # explicit -+# ./docker/build.sh eeschema # builds the schematic editor -+# ./docker/build.sh all # builds both, sequentially -+# ./docker/build.sh -j 8 ... # any extra args are forwarded to build-*.sh - # - # The build is split into two phases: - # 1. Docker: Compile KiCad to WASM (without asyncify) -@@ -10,14 +15,34 @@ source "$(dirname "$0")/../scripts/common/logging.sh" - # - # Binaryen is downloaded automatically - no prerequisites needed. - -+# Redirect all output to a log file (re-execs script with redirection) -+source "$(dirname "$0")/../scripts/common/logging.sh" -+ - set -e - - cd "$(dirname "$0")/.." - -+# First positional arg is the app name; everything else is forwarded to build-*.sh. -+APP_NAME="" -+if [[ $# -gt 0 ]] && [[ "$1" != -* ]]; then -+ APP_NAME="$1" -+ shift -+fi -+APP_NAME="${APP_NAME:-pcbnew}" -+ -+case "$APP_NAME" in -+ pcbnew|eeschema|all) ;; -+ *) -+ echo "Error: unknown app '$APP_NAME' (expected: pcbnew | eeschema | all)" >&2 -+ exit 1 -+ ;; -+esac -+ - # Use branch name as Docker Compose project name for isolated containers/volumes - BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD | tr '/' '-' | tr '[:upper:]' '[:lower:]') - export COMPOSE_PROJECT_NAME="kicad-wasm-${BRANCH_NAME}" - echo "Using Docker project: ${COMPOSE_PROJECT_NAME}" -+echo "Building app: ${APP_NAME}" - - # Add -j 10 by default if no -j flag is given - ARGS=("$@") -@@ -45,31 +70,42 @@ docker compose -f docker/docker-compose.yml exec kicad-wasm-builder \ - --exclude="tools/emsdk" \ - /workspace-host/ /workspace/ || [ $? -eq 24 ] - --# Run build command (without asyncify - handled on host due to memory requirements) --docker compose -f docker/docker-compose.yml exec kicad-wasm-builder \ -- /workspace/scripts/kicad/build-pcbnew.sh "${ARGS[@]}" -+# Build one app: compile in container, then run host-side post-processing. -+build_app() { -+ local app="$1" -+ echo "" -+ echo "=== Building ${app} ===" - --# Copy output to host-accessible directory --# Note: pcbnew.wasm.debug.wasm contains DWARF debug info (generated with -gseparate-dwarf) --echo "Copying build output to ./output/..." --docker compose -f docker/docker-compose.yml exec kicad-wasm-builder \ -- bash -c "mkdir -p /workspace/output && \ -- cp /workspace/build-wasm/kicad-pcbnew/pcbnew/pcbnew.{js,wasm,wasm.debug.wasm,wasm.map,worker.js} /workspace/output/ 2>/dev/null || \ -- cp /workspace/build-wasm/kicad-pcbnew/pcbnew/pcbnew.{js,wasm} /workspace/output/; \ -- cp /workspace/build-wasm/kicad-pcbnew/resources/images.tar.gz /workspace/output/ 2>/dev/null || true; \ -- cp /workspace/build-wasm/wxwidgets/build/wasm/wx.js /workspace/output/ 2>/dev/null || true" -- --# Inject dynCall shims into pcbnew.js --# This fixes "dynCall_* is not defined" errors in Emscripten 4.x --./scripts/common/inject-dyncall-shims.sh output/pcbnew.js -- --# Apply wasm-emscripten-finalize on host (skipped in Docker due to memory limits) --# This is done on the host because finalize with DWARF needs significant RAM --./scripts/common/apply-finalize.sh output/pcbnew.wasm output/pcbnew.wasm -- --# Apply asyncify transformation on host --# This is done on the host because wasm-opt --asyncify needs significant RAM --./scripts/common/apply-asyncify.sh output/pcbnew.wasm output/pcbnew.wasm -+ # Run build command (without asyncify - handled on host due to memory requirements) -+ docker compose -f docker/docker-compose.yml exec kicad-wasm-builder \ -+ "/workspace/scripts/kicad/build-${app}.sh" "${ARGS[@]}" -+ -+ # Copy output to host-accessible directory. -+ # ${app}.wasm.debug.wasm contains DWARF debug info (when built with -gseparate-dwarf). -+ echo "Copying ${app} build output to ./output/..." -+ docker compose -f docker/docker-compose.yml exec kicad-wasm-builder \ -+ bash -c "mkdir -p /workspace/output && \ -+ cp /workspace/build-wasm/kicad-${app}/${app}/${app}.{js,wasm,wasm.debug.wasm,wasm.map,worker.js} /workspace/output/ 2>/dev/null || \ -+ cp /workspace/build-wasm/kicad-${app}/${app}/${app}.{js,wasm} /workspace/output/; \ -+ cp /workspace/build-wasm/kicad-${app}/resources/images.tar.gz /workspace/output/ 2>/dev/null || true; \ -+ cp /workspace/build-wasm/wxwidgets/build/wasm/wx.js /workspace/output/ 2>/dev/null || true" -+ -+ # Inject dynCall shims (fixes "dynCall_* is not defined" errors in Emscripten 4.x) -+ ./scripts/common/inject-dyncall-shims.sh "output/${app}.js" -+ -+ # Apply wasm-emscripten-finalize on host (skipped in Docker due to memory limits) -+ ./scripts/common/apply-finalize.sh "output/${app}.wasm" "output/${app}.wasm" -+ -+ # Apply asyncify transformation on host -+ ./scripts/common/apply-asyncify.sh "output/${app}.wasm" "output/${app}.wasm" -+} -+ -+if [[ "${APP_NAME}" == "all" ]]; then -+ build_app pcbnew -+ build_app eeschema -+else -+ build_app "${APP_NAME}" -+fi - - echo "" - echo "Build complete. Output files in ./output/" -diff --git a/scripts/create-feature-patches.sh b/scripts/create-feature-patches.sh -index f6aa3e7..effae4e 100755 ---- a/scripts/create-feature-patches.sh -+++ b/scripts/create-feature-patches.sh -@@ -9,15 +9,35 @@ FEATURE_DIR="features/${BRANCH}" - - mkdir -p "$FEATURE_DIR" - --# Root repo patch (exclude submodules) --git diff HEAD -- ':!kicad' ':!wxwidgets' > "$FEATURE_DIR/root.patch" -+# Root repo patch (exclude submodules and features/ — the latter would cause -+# the patch to contain itself recursively). -+git diff HEAD -- ':!kicad' ':!wxwidgets' ':!features' > "$FEATURE_DIR/root.patch" - --# Submodule patches (diff from upstream base) --KICAD_BASE=$(git -C kicad log --format='%H' --author-not='viktor.vaczi@emergence-engineering.com' --author-not='noreply@anthropic.com' -1) --git -C kicad diff $KICAD_BASE > "$FEATURE_DIR/kicad.patch" -+# Submodule patches: diff against main's recorded submodule sha so the patch -+# captures only this feature branch's submodule work (committed + uncommitted), -+# never upstream changes that landed on main. -+sub_diff() { -+ local sub="$1" -+ local out="$2" -+ local main_sha -+ main_sha=$(git ls-tree origin/main "$sub" 2>/dev/null | awk '{print $3}') -+ if [ -z "$main_sha" ]; then -+ echo "Warning: could not resolve origin/main:$sub — skipping $out" >&2 -+ rm -f "$out" -+ return -+ fi -+ local cur_sha -+ cur_sha=$(git -C "$sub" rev-parse HEAD) -+ if [ "$main_sha" = "$cur_sha" ] && git -C "$sub" diff --quiet; then -+ echo "No feature-specific $sub changes (submodule pointer matches main, worktree clean) — skipping $(basename "$out")" -+ rm -f "$out" -+ return -+ fi -+ git -C "$sub" diff "$main_sha" > "$out" -+} - --WX_BASE="v3.2.6" --git -C wxwidgets diff $WX_BASE > "$FEATURE_DIR/wxwidgets.patch" -+sub_diff kicad "$FEATURE_DIR/kicad.patch" -+sub_diff wxwidgets "$FEATURE_DIR/wxwidgets.patch" - - echo "Patches created in $FEATURE_DIR/" - ls -la "$FEATURE_DIR"/*.patch 2>/dev/null || echo "No patches generated" -diff --git a/scripts/kicad/build-eeschema.sh b/scripts/kicad/build-eeschema.sh -new file mode 100755 -index 0000000..216bd1f ---- /dev/null -+++ b/scripts/kicad/build-eeschema.sh -@@ -0,0 +1,7 @@ -+#!/bin/bash -+# Build KiCad Eeschema (schematic 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" eeschema "$@" -diff --git a/scripts/kicad/build-kicad-target.sh b/scripts/kicad/build-kicad-target.sh -new file mode 100755 -index 0000000..5685fe0 ---- /dev/null -+++ b/scripts/kicad/build-kicad-target.sh -@@ -0,0 +1,395 @@ -+#!/bin/bash -+# Build a KiCad editor (pcbnew or eeschema) for WebAssembly. -+# -+# Usage: -+# ./scripts/kicad/build-kicad-target.sh [options] -+# -+# Args: -+# pcbnew | eeschema (required) -+# -+# Options: -+# --full Full clean rebuild (dependencies + KiCad) -+# --clean-kicad Clean only KiCad build directory (not deps) -+# --build-deps Build dependencies (default: skip) -+# --debug Build with debug symbols (default) -+# --release Build optimized without debug symbols -+# --diag=... Diagnostic preprocessor flags (gal, coroutine, ctor, all) -+# -j N Parallel compilation jobs (default: 1) -+# -+# Each editor builds into its own tree: build-wasm/kicad-/. -+# Per-editor extras live alongside generic stubs: -+# - wasm/bindings/_embind.cpp (optional) -+# - wasm/stubs/_frame_stub.cpp (optional, app-specific stubs) -+# - wasm/stubs/_scripting_stub.cpp (optional, app-specific scripting stubs) -+ -+set -e -+ -+if [ -z "$1" ]; then -+ echo "Error: missing argument (pcbnew | eeschema)" >&2 -+ exit 1 -+fi -+APP_NAME="$1" -+shift -+ -+case "$APP_NAME" in -+ pcbnew|eeschema) ;; -+ *) -+ echo "Error: unknown app '$APP_NAME' (expected: pcbnew | eeschema)" >&2 -+ exit 1 -+ ;; -+esac -+ -+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -+source "${SCRIPT_DIR}/../common/env.sh" -+source "${SCRIPT_DIR}/../common/versions.sh" -+source "${SCRIPT_DIR}/../common/functions.sh" -+ -+KICAD_DIR="${PROJECT_ROOT}/kicad" -+KICAD_BUILD="${BUILD_ROOT}/kicad-${APP_NAME}" -+KICAD_STAMP="${BUILD_ROOT}/stamps/kicad-${APP_NAME}.stamp" -+WASM_LAYER="${PROJECT_ROOT}/wasm" -+WX_BUILD="${BUILD_ROOT}/wxwidgets-universal" -+ -+# Parse arguments - incremental build by default (optimized for development) -+NO_CLEAN=1 -+FULL_CLEAN=0 -+SKIP_DEPS=1 -+DEBUG=0 -+DIAG_LIST="" -+while [[ $# -gt 0 ]]; do -+ case $1 in -+ --full) -+ FULL_CLEAN=1 -+ NO_CLEAN=0 -+ SKIP_DEPS=0 -+ shift -+ ;; -+ --clean-kicad) -+ NO_CLEAN=0 -+ shift -+ ;; -+ --build-deps) -+ SKIP_DEPS=0 -+ shift -+ ;; -+ --debug) -+ DEBUG=1 -+ shift -+ ;; -+ --release) -+ DEBUG_BUILD=0 -+ export DEBUG_BUILD -+ shift -+ ;; -+ --diag=*) -+ DIAG_LIST="${1#--diag=}" -+ shift -+ ;; -+ --diag) -+ DIAG_LIST="$2" -+ shift 2 -+ ;; -+ -j) -+ export JOBS="$2" -+ shift 2 -+ ;; -+ -j*) -+ export JOBS="${1#-j}" -+ shift -+ ;; -+ *) -+ shift -+ ;; -+ esac -+done -+ -+# Diagnostic preprocessor defines from --diag= (gal, coroutine, ctor, all). -+# These gate the KI_DIAG_* macros in kicad/include/kicad_wasm_diag.h. Output goes -+# to stdout ([KICAD_OUT] logs), never errors. Off by default. -+DIAG_DEFINES="" -+if [ -n "${DIAG_LIST}" ]; then -+ IFS=',' read -ra _diag_cats <<< "${DIAG_LIST}" -+ for _cat in "${_diag_cats[@]}"; do -+ case "${_cat}" in -+ gal) DIAG_DEFINES="${DIAG_DEFINES} -DKICAD_DIAG_GAL=1" ;; -+ coroutine) DIAG_DEFINES="${DIAG_DEFINES} -DKICAD_DIAG_COROUTINE=1" ;; -+ ctor) DIAG_DEFINES="${DIAG_DEFINES} -DKICAD_DIAG_CTOR=1" ;; -+ all) DIAG_DEFINES="${DIAG_DEFINES} -DKICAD_DIAG_GAL=1 -DKICAD_DIAG_COROUTINE=1 -DKICAD_DIAG_CTOR=1" ;; -+ "") ;; -+ *) log_warn "Unknown --diag category: '${_cat}' (valid: gal, coroutine, ctor, all)" ;; -+ esac -+ done -+ log_info "Diagnostic logging enabled:${DIAG_DEFINES}" -+fi -+ -+log_info "Building app: ${APP_NAME}" -+log_info "Using ${JOBS} parallel jobs" -+ -+# Step 1: Clean build directories -+if [ $FULL_CLEAN -eq 1 ]; then -+ log_info "Full clean: removing all stamps and build directories..." -+ rm -rf "${STAMPS_DIR}"/* -+ rm -rf "${BUILD_ROOT}/deps"/* -+ rm -rf "${BUILD_ROOT}/wxwidgets-universal" -+ rm -rf "${BUILD_ROOT}/stubs" -+ rm -rf "${KICAD_BUILD}" -+ rm -rf "${SYSROOT}"/* -+elif [ $NO_CLEAN -eq 0 ]; then -+ log_info "Cleaning KiCad ${APP_NAME} build directory..." -+ rm -rf "${KICAD_BUILD}" "${KICAD_STAMP}" -+else -+ log_info "Incremental build (use --clean-kicad or --full to clean)" -+fi -+ -+# Step 2: Build dependencies -+# Note: --with-occ for OpenCASCADE, but NOT ngspice since KICAD_SPICE=OFF -+if [ $SKIP_DEPS -eq 0 ]; then -+ log_info "Building dependencies..." -+ "${SCRIPT_DIR}/../deps/build-all-deps.sh" --with-occ -+else -+ log_info "Skipping dependencies (use --build-deps or --full to build)" -+fi -+ -+# Note: We don't check the KiCad stamp here for incremental builds. -+# CMake handles dependency tracking - it will detect changed source files -+# and only recompile what's needed. The stamp is created at the end for -+# scripts that want to know if KiCad was ever built successfully. -+ -+# Step 4: Build wxWidgets (incremental - only recompiles changed files) -+log_info "Building wxWidgets..." -+"${SCRIPT_DIR}/../build-wxuniversal-wasm.sh" --no-clean -+ -+log_info "Building KiCad ${APP_NAME} ${KICAD_VERSION} for WASM..." -+ -+# Step 5: Set build type -+# Use environment DEBUG_BUILD if set, otherwise check local --debug flag -+# -fexceptions is required because wxWidgets is built with exceptions enabled -+# -matomics -mbulk-memory are required for shared memory (pthreads) -+# NOTE: We use -O1 for debug builds because -O0 produces WASM with too many -+# locals for V8/Chrome to compile (error: "local count too large"). -+# -O1 keeps debug info but optimizes enough to stay under V8's limits. -+if [ "${DEBUG_BUILD:-0}" = "1" ] || [ $DEBUG -eq 1 ]; then -+ BUILD_TYPE="Debug" -+ EXTRA_FLAGS="-g -O1 -fexceptions -matomics -mbulk-memory" -+ # -gseparate-dwarf puts debug info in a separate .debug.wasm file -+ # This keeps the main WASM small (~200MB) while preserving full debug info -+ # DevTools loads the debug file on-demand when debugging -+ LINKER_DEBUG_FLAGS="-O1 -g -gseparate-dwarf -fexceptions" -+ log_info "Building KiCad in DEBUG mode (separate DWARF for smaller main binary)" -+else -+ BUILD_TYPE="Release" -+ EXTRA_FLAGS="-O2 -fexceptions -matomics -mbulk-memory" -+ # -O0 at link time skips wasm-opt (which can OOM on large WASM files) -+ # Compilation is still -O2 for optimized code, but we skip post-link wasm-opt -+ LINKER_DEBUG_FLAGS="-O0 -fexceptions" -+ log_info "Building KiCad in RELEASE mode (skipping wasm-opt due to memory limits)" -+fi -+ -+# Step 6: Create build directory -+mkdir -p "${KICAD_BUILD}" -+cd "${KICAD_BUILD}" -+ -+# Step 6.1: Build stub libraries for missing symbols -+# Generic stubs (libgit2, curl, nng) are shared across apps and built in BUILD_ROOT/stubs. -+# App-specific stubs (e.g. pcbnew_scripting_stub) build into the same directory but -+# are only linked in when the corresponding source exists. -+STUBS_DIR="${PROJECT_ROOT}/wasm/stubs" -+STUBS_BUILD="${BUILD_ROOT}/stubs" -+mkdir -p "${STUBS_BUILD}" -+ -+log_info "Building stub libraries..." -+# Compile libgit2 stub -+emcc -c "${STUBS_DIR}/libgit2_stub.c" -o "${STUBS_BUILD}/libgit2_stub.o" -+emar rcs "${STUBS_BUILD}/libgit2_stub.a" "${STUBS_BUILD}/libgit2_stub.o" -+ -+# Compile curl stub -+emcc -c "${STUBS_DIR}/curl_stub.c" -o "${STUBS_BUILD}/curl_stub.o" -+emar rcs "${STUBS_BUILD}/libcurl_stub.a" "${STUBS_BUILD}/curl_stub.o" -+ -+# Note: GLU tesselator is now implemented in wasm/stubs/glu_wasm_impl.cpp -+# It's compiled as part of the GAL library (requires KiCad headers) -+ -+# Compile NNG stub (IPC API requires NNG but sockets don't work in WASM) -+emcc -c -I"${STUBS_DIR}" "${STUBS_DIR}/nng_stub.c" -o "${STUBS_BUILD}/nng_stub.o" -+emar rcs "${STUBS_BUILD}/libnng_stub.a" "${STUBS_BUILD}/nng_stub.o" -+ -+# wx flags for any C++ stubs that include wx headers -+WX_CXXFLAGS=$("${WX_BUILD}/wx-config" --cxxflags 2>/dev/null || echo "-I${WX_BUILD}/lib/wx/include/emscripten-unicode-static-3.2 -I${PROJECT_ROOT}/wxwidgets/include") -+ -+# App-specific stubs: -+# - pcbnew: pcbnew_scripting_stub.cpp (action-plugin scripting placeholders) -+# - eeschema: eeschema_frame_stub.cpp (placeholder; grows as linker dictates) -+APP_STUB_LINK="" -+APP_SCRIPTING_STUB_SRC="${STUBS_DIR}/${APP_NAME}_scripting_stub.cpp" -+if [ -f "${APP_SCRIPTING_STUB_SRC}" ]; then -+ log_info "Building app scripting stub: ${APP_NAME}_scripting_stub.cpp" -+ em++ -c ${WX_CXXFLAGS} "${APP_SCRIPTING_STUB_SRC}" -o "${STUBS_BUILD}/${APP_NAME}_scripting_stub.o" -+ emar rcs "${STUBS_BUILD}/lib${APP_NAME}_scripting_stub.a" "${STUBS_BUILD}/${APP_NAME}_scripting_stub.o" -+ APP_STUB_LINK="${APP_STUB_LINK} ${STUBS_BUILD}/lib${APP_NAME}_scripting_stub.a" -+fi -+ -+APP_FRAME_STUB_SRC="${STUBS_DIR}/${APP_NAME}_frame_stub.cpp" -+if [ -f "${APP_FRAME_STUB_SRC}" ] && [ -s "${APP_FRAME_STUB_SRC}" ]; then -+ log_info "Building app frame stub: ${APP_NAME}_frame_stub.cpp" -+ em++ -c ${WX_CXXFLAGS} "${APP_FRAME_STUB_SRC}" -o "${STUBS_BUILD}/${APP_NAME}_frame_stub.o" -+ emar rcs "${STUBS_BUILD}/lib${APP_NAME}_frame_stub.a" "${STUBS_BUILD}/${APP_NAME}_frame_stub.o" -+ APP_STUB_LINK="${APP_STUB_LINK} ${STUBS_BUILD}/lib${APP_NAME}_frame_stub.a" -+fi -+ -+log_info "Stub libraries built" -+ -+# Step 6.2: Replace Emscripten's wasm-opt with stub to bypass asyncify transformation -+# This allows Emscripten to generate JS with Asyncify runtime, but we run the real -+# wasm-opt --asyncify on the host where more RAM is available (needs 50GB+ for KiCad) -+if [ -z "${EMSDK}" ]; then -+ log_error "EMSDK environment variable is not set." -+ exit 1 -+fi -+EMSDK_WASM_OPT="${EMSDK}/upstream/bin/wasm-opt" -+if [ -f "${EMSDK_WASM_OPT}" ] && [ ! -f "${EMSDK_WASM_OPT}.real" ]; then -+ log_info "Backing up real wasm-opt..." -+ mv "${EMSDK_WASM_OPT}" "${EMSDK_WASM_OPT}.real" -+fi -+# Always copy the latest stub (in case it was updated) -+cp "${STUBS_DIR}/wasm-opt-stub.sh" "${EMSDK_WASM_OPT}" -+chmod +x "${EMSDK_WASM_OPT}" -+log_info "wasm-opt stub installed (asyncify will run on host)" -+ -+# Step 6.3: Replace wasm-emscripten-finalize with stub (same pattern as wasm-opt) -+# This tool also OOMs on large WASM with debug symbols, so we run it on the host -+EMSDK_FINALIZE="${EMSDK}/upstream/bin/wasm-emscripten-finalize" -+if [ -f "${EMSDK_FINALIZE}" ] && [ ! -f "${EMSDK_FINALIZE}.real" ]; then -+ log_info "Backing up real wasm-emscripten-finalize..." -+ mv "${EMSDK_FINALIZE}" "${EMSDK_FINALIZE}.real" -+fi -+# Always copy the latest stub (in case it was updated) -+cp "${STUBS_DIR}/wasm-emscripten-finalize-stub.sh" "${EMSDK_FINALIZE}" -+chmod +x "${EMSDK_FINALIZE}" -+log_info "wasm-emscripten-finalize stub installed (finalize will run on host)" -+ -+# Step 6.5: Verify WASM support is in KiCad fork -+# The kicad submodule should already have WASM port detection and kiplatform support -+KICAD_CMAKE="${KICAD_DIR}/CMakeLists.txt" -+if ! grep -q "msw|qt|gtk|osx|wasm" "${KICAD_CMAKE}"; then -+ log_error "KiCad fork is missing WASM port detection support." -+ log_error "Please ensure the kicad submodule has WASM modifications." -+ exit 1 -+fi -+KIPLATFORM_CMAKE="${KICAD_DIR}/libs/kiplatform/CMakeLists.txt" -+if ! grep -q "KICAD_WX_PORT STREQUAL wasm" "${KIPLATFORM_CMAKE}"; then -+ log_error "KiCad fork is missing kiplatform WASM support." -+ log_error "Please ensure the kicad submodule has WASM modifications." -+ exit 1 -+fi -+log_info "KiCad WASM support verified" -+ -+# Embind object — built after CMake configure runs (so config.h exists). The -+# linker line below references "${STUBS_BUILD}/${APP_NAME}_embind.o" so we -+# create an empty placeholder when the source is missing, to keep the link -+# line stable across apps. -+EMBIND_OBJ="${STUBS_BUILD}/${APP_NAME}_embind.o" -+EMBIND_SRC="${PROJECT_ROOT}/wasm/bindings/${APP_NAME}_embind.cpp" -+ -+# Step 7: Configure KiCad with CMake -+# We use CMAKE_MODULE_PATH to inject our compatibility layer -+log_info "Configuring KiCad with CMake..." -+ -+# Use ccache if available (CMAKE_*_COMPILER_LAUNCHER is the proper CMake way) -+CCACHE_OPTS="" -+if command -v ccache &> /dev/null; then -+ CCACHE_OPTS="-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache" -+ log_info "Using ccache for compilation" -+fi -+ -+emcmake cmake "${KICAD_DIR}" \ -+ ${CCACHE_OPTS} \ -+ -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ -+ -DCMAKE_INSTALL_PREFIX="${SYSROOT}" \ -+ -DCMAKE_MODULE_PATH="${WASM_LAYER}/cmake" \ -+ -DSYSROOT="${SYSROOT}" \ -+ -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ -+ -DCMAKE_CXX_FLAGS="${EXTRA_FLAGS} -pthread -sUSE_ZLIB=1 -DKICAD_USE_PLATFORM_WASM=1${DIAG_DEFINES} -I${SYSROOT}/include -I${STUBS_DIR} -include ${STUBS_DIR}/char_traits_uint16_workaround.h" \ -+ -DCMAKE_C_FLAGS="${EXTRA_FLAGS} -pthread -sUSE_ZLIB=1 -I${SYSROOT}/include -I${STUBS_DIR}" \ -+ -DCMAKE_EXE_LINKER_FLAGS="${LINKER_DEBUG_FLAGS} -pthread -sUSE_ZLIB=1 -sASYNCIFY=1 -sDYNCALLS=1 -sASYNCIFY_STACK_SIZE=65536 -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE='navigator.hardwareConcurrency' -sPTHREAD_POOL_SIZE_STRICT=0 -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=256MB -sMAXIMUM_MEMORY=4GB -sMAX_WEBGL_VERSION=2 -sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8','dynCall'] -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=['\$dynCall'] --bind -L${SYSROOT}/lib ${STUBS_BUILD}/libgit2_stub.a ${STUBS_BUILD}/libcurl_stub.a${APP_STUB_LINK} ${STUBS_BUILD}/libnng_stub.a ${EMBIND_OBJ}" \ -+ -DCMAKE_PREFIX_PATH="${SYSROOT};${WX_BUILD}" \ -+ -DwxWidgets_CONFIG_EXECUTABLE="${WX_BUILD}/wx-config" \ -+ \ -+ -DKICAD_BUILD_QA_TESTS=OFF \ -+ -DKICAD_SPICE=OFF \ -+ -DKICAD_USE_EGL=OFF \ -+ -DKICAD_USE_BUNDLED_GLEW=ON \ -+ -DKICAD_BUILD_3D_VIEWER_WASM=OFF \ -+ -DKICAD_IPC_API=ON \ -+ -DKICAD_USE_PCH=ON \ -+ \ -+ -DZSTD_ROOT="${SYSROOT}" \ -+ -DZSTD_INCLUDE_DIR="${SYSROOT}/include" \ -+ -DZSTD_LIBRARY="${SYSROOT}/lib/libzstd.a" \ -+ -DGLM_INCLUDE_DIR="${SYSROOT}/include" \ -+ -DGLM_VERSION="0.9.9.8" \ -+ -DBOOST_ROOT="${SYSROOT}" \ -+ -DBoost_INCLUDE_DIR="${SYSROOT}/include" \ -+ -DBoost_LIBRARY_DIR="${SYSROOT}/lib" \ -+ -DBoost_NO_SYSTEM_PATHS=ON \ -+ -DBoost_NO_BOOST_CMAKE=ON \ -+ -DFREETYPE_INCLUDE_DIR_ft2build="${SYSROOT}/include/freetype2" \ -+ -DFREETYPE_INCLUDE_DIR_freetype2="${SYSROOT}/include/freetype2" \ -+ -DFREETYPE_LIBRARY="${SYSROOT}/lib/libfreetype.a" \ -+ -DHarfBuzz_INCLUDE_DIR="${SYSROOT}/include/harfbuzz" \ -+ -DHarfBuzz_LIBRARY="${SYSROOT}/lib/libharfbuzz.a" \ -+ -DOCC_INCLUDE_DIR="${SYSROOT}/include/opencascade" \ -+ -DOCC_LIBRARY_DIR="${SYSROOT}/lib" \ -+ -DProtobuf_INCLUDE_DIR="${SYSROOT}/include" \ -+ -DProtobuf_LIBRARY="${SYSROOT}/lib/libprotobuf.a" \ -+ -DProtobuf_LITE_LIBRARY="${SYSROOT}/lib/libprotobuf-lite.a" \ -+ -DProtobuf_PROTOC_EXECUTABLE="${SYSROOT}/bin/protoc" \ -+ -DODBC_CONFIG:STRING="stub-for-wasm" \ -+ -DODBCLIB:STRING="" \ -+ -DODBC_CFLAGS:STRING="" \ -+ -DODBC_LINK_FLAGS:STRING="" \ -+ -DODBC_LIBRARIES:STRING="" \ -+ \ -+ -DBUILD_GITHUB_PLUGIN=OFF \ -+ -DKICAD_PCM=OFF \ -+ \ -+ -DHAVE_STRCASECMP=1 \ -+ -DHAVE_STRNCASECMP=1 -+ -+# Step 7.1: Compile Embind bindings (after CMake so config.h exists) -+# Exposes KiCad objects to JavaScript for future Pyodide integration. -+# When no app-specific source exists, build an empty object so the linker line -+# referencing ${APP_NAME}_embind.o doesn't break. -+if [ -f "${EMBIND_SRC}" ]; then -+ log_info "Compiling Embind bindings (${APP_NAME})..." -+ # Use the same includes and flags that KiCad uses -+ KICAD_INCLUDES="-I${KICAD_BUILD} -I${KICAD_DIR}/include -I${KICAD_DIR}/${APP_NAME} -I${KICAD_DIR}/common" -+ KICAD_INCLUDES+=" -I${KICAD_DIR}/libs/core/include -I${KICAD_DIR}/libs/kimath/include -I${KICAD_DIR}/libs/kiplatform/include" -+ KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/clipper2/Clipper2Lib/include" -+ KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/nlohmann_json" -+ KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/dynamic_bitset" -+ KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/nanodbc" -+ KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/picosha2" -+ KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty" -+ KICAD_INCLUDES+=" -I${SYSROOT}/include" -+ # KiCad requires C++20 for concepts -+ em++ -std=c++20 -c ${EXTRA_FLAGS} ${WX_CXXFLAGS} ${KICAD_INCLUDES} "${EMBIND_SRC}" -o "${EMBIND_OBJ}" -+else -+ log_info "No embind source for ${APP_NAME} (expected at ${EMBIND_SRC}); using empty placeholder" -+ EMPTY_C="${STUBS_BUILD}/${APP_NAME}_embind_empty.c" -+ : > "${EMPTY_C}" -+ emcc -c "${EMPTY_C}" -o "${EMBIND_OBJ}" -+fi -+ -+# Step 8: Build the app target -+log_info "Building ${APP_NAME}..." -+emmake make -j${JOBS} "${APP_NAME}" -+ -+# Step 8.1: Build bitmap resources (images.tar.gz) -+# This creates the icon archive that KiCad loads at runtime -+log_info "Building bitmap resources..." -+emmake make bitmap_archive_build -+ -+# Step 9: Create stamp file -+create_stamp "${KICAD_STAMP}" -+log_info "KiCad ${APP_NAME} build complete!" -+log_info "Output: ${KICAD_BUILD}/${APP_NAME}/${APP_NAME}.js" -diff --git a/scripts/kicad/build-pcbnew.sh b/scripts/kicad/build-pcbnew.sh -index fc1044f..4487c97 100755 ---- a/scripts/kicad/build-pcbnew.sh -+++ b/scripts/kicad/build-pcbnew.sh -@@ -1,349 +1,7 @@ - #!/bin/bash --# Build KiCad PCBnew for WebAssembly --# This builds the PCB editor as a standalone WASM application --# --# Usage: --# ./scripts/kicad/build-pcbnew.sh [options] --# --# Options: --# --full Full clean rebuild (dependencies + KiCad) --# --clean-kicad Clean only KiCad build directory (not deps) --# --build-deps Build dependencies (default: skip) --# --debug Build with debug symbols (default) --# --release Build optimized without debug symbols --# -j N Parallel compilation jobs (default: 1) --# --# Defaults (optimized for development): --# - Incremental build (no clean) --# - Skip dependencies --# - ccache enabled for faster rebuilds --# --# Incremental Build System: --# - wxWidgets: configure runs once, make handles file-level dependencies --# - KiCad: CMake tracks dependencies, only recompiles changed files --# - ccache: Caches compiled objects for faster rebuilds -+# Build KiCad PCBnew for WebAssembly. -+# Thin wrapper around build-kicad-target.sh — see that script for options. - - set -e -- - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" --source "${SCRIPT_DIR}/../common/env.sh" --source "${SCRIPT_DIR}/../common/versions.sh" --source "${SCRIPT_DIR}/../common/functions.sh" -- --KICAD_DIR="${PROJECT_ROOT}/kicad" --KICAD_BUILD="${BUILD_ROOT}/kicad-pcbnew" --KICAD_STAMP="${BUILD_ROOT}/stamps/kicad-pcbnew.stamp" --WASM_LAYER="${PROJECT_ROOT}/wasm" --WX_BUILD="${BUILD_ROOT}/wxwidgets-universal" -- --# Parse arguments - incremental build by default (optimized for development) --NO_CLEAN=1 --FULL_CLEAN=0 --SKIP_DEPS=1 --DEBUG=0 --DIAG_LIST="" --while [[ $# -gt 0 ]]; do -- case $1 in -- --full) -- FULL_CLEAN=1 -- NO_CLEAN=0 -- SKIP_DEPS=0 -- shift -- ;; -- --clean-kicad) -- NO_CLEAN=0 -- shift -- ;; -- --build-deps) -- SKIP_DEPS=0 -- shift -- ;; -- --debug) -- DEBUG=1 -- shift -- ;; -- --release) -- DEBUG_BUILD=0 -- export DEBUG_BUILD -- shift -- ;; -- --diag=*) -- DIAG_LIST="${1#--diag=}" -- shift -- ;; -- --diag) -- DIAG_LIST="$2" -- shift 2 -- ;; -- -j) -- export JOBS="$2" -- shift 2 -- ;; -- -j*) -- export JOBS="${1#-j}" -- shift -- ;; -- *) -- shift -- ;; -- esac --done -- --# Diagnostic preprocessor defines from --diag= (gal, coroutine, ctor, all). --# These gate the KI_DIAG_* macros in kicad/include/kicad_wasm_diag.h. Output goes --# to stdout ([KICAD_OUT] logs), never errors. Off by default. --DIAG_DEFINES="" --if [ -n "${DIAG_LIST}" ]; then -- IFS=',' read -ra _diag_cats <<< "${DIAG_LIST}" -- for _cat in "${_diag_cats[@]}"; do -- case "${_cat}" in -- gal) DIAG_DEFINES="${DIAG_DEFINES} -DKICAD_DIAG_GAL=1" ;; -- coroutine) DIAG_DEFINES="${DIAG_DEFINES} -DKICAD_DIAG_COROUTINE=1" ;; -- ctor) DIAG_DEFINES="${DIAG_DEFINES} -DKICAD_DIAG_CTOR=1" ;; -- all) DIAG_DEFINES="${DIAG_DEFINES} -DKICAD_DIAG_GAL=1 -DKICAD_DIAG_COROUTINE=1 -DKICAD_DIAG_CTOR=1" ;; -- "") ;; -- *) log_warn "Unknown --diag category: '${_cat}' (valid: gal, coroutine, ctor, all)" ;; -- esac -- done -- log_info "Diagnostic logging enabled:${DIAG_DEFINES}" --fi -- --log_info "Using ${JOBS} parallel jobs" -- --# Step 1: Clean build directories --if [ $FULL_CLEAN -eq 1 ]; then -- log_info "Full clean: removing all stamps and build directories..." -- rm -rf "${STAMPS_DIR}"/* -- rm -rf "${BUILD_ROOT}/deps"/* -- rm -rf "${BUILD_ROOT}/wxwidgets-universal" -- rm -rf "${BUILD_ROOT}/stubs" -- rm -rf "${KICAD_BUILD}" -- rm -rf "${SYSROOT}"/* --elif [ $NO_CLEAN -eq 0 ]; then -- log_info "Cleaning KiCad PCBnew build directory..." -- rm -rf "${KICAD_BUILD}" "${KICAD_STAMP}" --else -- log_info "Incremental build (use --clean-kicad or --full to clean)" --fi -- --# Step 2: Build dependencies --# Note: --with-occ for OpenCASCADE, but NOT ngspice since KICAD_SPICE=OFF --if [ $SKIP_DEPS -eq 0 ]; then -- log_info "Building dependencies..." -- "${SCRIPT_DIR}/../deps/build-all-deps.sh" --with-occ --else -- log_info "Skipping dependencies (use --build-deps or --full to build)" --fi -- --# Note: We don't check the KiCad stamp here for incremental builds. --# CMake handles dependency tracking - it will detect changed source files --# and only recompile what's needed. The stamp is created at the end for --# scripts that want to know if KiCad was ever built successfully. -- --# Step 4: Build wxWidgets (incremental - only recompiles changed files) --# The wxWidgets build script handles: --# - Skipping configure if already configured --# - make handles per-file dependency tracking --# - ccache handles compilation caching --log_info "Building wxWidgets..." --"${SCRIPT_DIR}/../build-wxuniversal-wasm.sh" --no-clean -- --log_info "Building KiCad PCBnew ${KICAD_VERSION} for WASM..." -- --# Step 5: Set build type --# Use environment DEBUG_BUILD if set, otherwise check local --debug flag --# -fexceptions is required because wxWidgets is built with exceptions enabled --# -matomics -mbulk-memory are required for shared memory (pthreads) --# NOTE: We use -O1 for debug builds because -O0 produces WASM with too many --# locals for V8/Chrome to compile (error: "local count too large"). --# -O1 keeps debug info but optimizes enough to stay under V8's limits. --if [ "${DEBUG_BUILD:-0}" = "1" ] || [ $DEBUG -eq 1 ]; then -- BUILD_TYPE="Debug" -- EXTRA_FLAGS="-g -O1 -fexceptions -matomics -mbulk-memory" -- # -gseparate-dwarf puts debug info in a separate .debug.wasm file -- # This keeps the main WASM small (~200MB) while preserving full debug info -- # DevTools loads the debug file on-demand when debugging -- LINKER_DEBUG_FLAGS="-O1 -g -gseparate-dwarf -fexceptions" -- log_info "Building KiCad in DEBUG mode (separate DWARF for smaller main binary)" --else -- BUILD_TYPE="Release" -- EXTRA_FLAGS="-O2 -fexceptions -matomics -mbulk-memory" -- # -O0 at link time skips wasm-opt (which can OOM on large WASM files) -- # Compilation is still -O2 for optimized code, but we skip post-link wasm-opt -- LINKER_DEBUG_FLAGS="-O0 -fexceptions" -- log_info "Building KiCad in RELEASE mode (skipping wasm-opt due to memory limits)" --fi -- --# Step 6: Create build directory --mkdir -p "${KICAD_BUILD}" --cd "${KICAD_BUILD}" -- --# Step 6.1: Build stub libraries for missing symbols --STUBS_DIR="${PROJECT_ROOT}/wasm/stubs" --STUBS_BUILD="${BUILD_ROOT}/stubs" --mkdir -p "${STUBS_BUILD}" -- --log_info "Building stub libraries..." --# Compile libgit2 stub --emcc -c "${STUBS_DIR}/libgit2_stub.c" -o "${STUBS_BUILD}/libgit2_stub.o" --emar rcs "${STUBS_BUILD}/libgit2_stub.a" "${STUBS_BUILD}/libgit2_stub.o" -- --# Compile curl stub --emcc -c "${STUBS_DIR}/curl_stub.c" -o "${STUBS_BUILD}/curl_stub.o" --emar rcs "${STUBS_BUILD}/libcurl_stub.a" "${STUBS_BUILD}/curl_stub.o" -- --# Note: GLU tesselator is now implemented in wasm/stubs/glu_wasm_impl.cpp --# It's compiled as part of the GAL library (requires KiCad headers) -- --# Compile PCBnew scripting stub (requires wxWidgets headers) --WX_CXXFLAGS=$("${WX_BUILD}/wx-config" --cxxflags 2>/dev/null || echo "-I${WX_BUILD}/lib/wx/include/emscripten-unicode-static-3.2 -I${PROJECT_ROOT}/wxwidgets/include") --em++ -c ${WX_CXXFLAGS} "${STUBS_DIR}/pcbnew_scripting_stub.cpp" -o "${STUBS_BUILD}/pcbnew_scripting_stub.o" --emar rcs "${STUBS_BUILD}/libpcbnew_scripting_stub.a" "${STUBS_BUILD}/pcbnew_scripting_stub.o" -- --# Compile NNG stub (IPC API requires NNG but sockets don't work in WASM) --emcc -c -I"${STUBS_DIR}" "${STUBS_DIR}/nng_stub.c" -o "${STUBS_BUILD}/nng_stub.o" --emar rcs "${STUBS_BUILD}/libnng_stub.a" "${STUBS_BUILD}/nng_stub.o" -- --log_info "Stub libraries built" -- --# Step 6.2: Replace Emscripten's wasm-opt with stub to bypass asyncify transformation --# This allows Emscripten to generate JS with Asyncify runtime, but we run the real --# wasm-opt --asyncify on the host where more RAM is available (needs 50GB+ for KiCad) --if [ -z "${EMSDK}" ]; then -- log_error "EMSDK environment variable is not set." -- exit 1 --fi --EMSDK_WASM_OPT="${EMSDK}/upstream/bin/wasm-opt" --if [ -f "${EMSDK_WASM_OPT}" ] && [ ! -f "${EMSDK_WASM_OPT}.real" ]; then -- log_info "Backing up real wasm-opt..." -- mv "${EMSDK_WASM_OPT}" "${EMSDK_WASM_OPT}.real" --fi --# Always copy the latest stub (in case it was updated) --cp "${STUBS_DIR}/wasm-opt-stub.sh" "${EMSDK_WASM_OPT}" --chmod +x "${EMSDK_WASM_OPT}" --log_info "wasm-opt stub installed (asyncify will run on host)" -- --# Step 6.3: Replace wasm-emscripten-finalize with stub (same pattern as wasm-opt) --# This tool also OOMs on large WASM with debug symbols, so we run it on the host --EMSDK_FINALIZE="${EMSDK}/upstream/bin/wasm-emscripten-finalize" --if [ -f "${EMSDK_FINALIZE}" ] && [ ! -f "${EMSDK_FINALIZE}.real" ]; then -- log_info "Backing up real wasm-emscripten-finalize..." -- mv "${EMSDK_FINALIZE}" "${EMSDK_FINALIZE}.real" --fi --# Always copy the latest stub (in case it was updated) --cp "${STUBS_DIR}/wasm-emscripten-finalize-stub.sh" "${EMSDK_FINALIZE}" --chmod +x "${EMSDK_FINALIZE}" --log_info "wasm-emscripten-finalize stub installed (finalize will run on host)" -- --# Step 6.5: Verify WASM support is in KiCad fork --# The kicad submodule should already have WASM port detection and kiplatform support --KICAD_CMAKE="${KICAD_DIR}/CMakeLists.txt" --if ! grep -q "msw|qt|gtk|osx|wasm" "${KICAD_CMAKE}"; then -- log_error "KiCad fork is missing WASM port detection support." -- log_error "Please ensure the kicad submodule has WASM modifications." -- exit 1 --fi --KIPLATFORM_CMAKE="${KICAD_DIR}/libs/kiplatform/CMakeLists.txt" --if ! grep -q "KICAD_WX_PORT STREQUAL wasm" "${KIPLATFORM_CMAKE}"; then -- log_error "KiCad fork is missing kiplatform WASM support." -- log_error "Please ensure the kicad submodule has WASM modifications." -- exit 1 --fi --log_info "KiCad WASM support verified" -- --# Step 7: Configure KiCad with CMake --# We use CMAKE_MODULE_PATH to inject our compatibility layer --log_info "Configuring KiCad with CMake..." -- --# Use ccache if available (CMAKE_*_COMPILER_LAUNCHER is the proper CMake way) --CCACHE_OPTS="" --if command -v ccache &> /dev/null; then -- CCACHE_OPTS="-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache" -- log_info "Using ccache for compilation" --fi -- --emcmake cmake "${KICAD_DIR}" \ -- ${CCACHE_OPTS} \ -- -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ -- -DCMAKE_INSTALL_PREFIX="${SYSROOT}" \ -- -DCMAKE_MODULE_PATH="${WASM_LAYER}/cmake" \ -- -DSYSROOT="${SYSROOT}" \ -- -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ -- -DCMAKE_CXX_FLAGS="${EXTRA_FLAGS} -pthread -sUSE_ZLIB=1 -DKICAD_USE_PLATFORM_WASM=1${DIAG_DEFINES} -I${SYSROOT}/include -I${STUBS_DIR}" \ -- -DCMAKE_C_FLAGS="${EXTRA_FLAGS} -pthread -sUSE_ZLIB=1 -I${SYSROOT}/include -I${STUBS_DIR}" \ -- -DCMAKE_EXE_LINKER_FLAGS="${LINKER_DEBUG_FLAGS} -pthread -sUSE_ZLIB=1 -sASYNCIFY=1 -sDYNCALLS=1 -sASYNCIFY_STACK_SIZE=65536 -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE='navigator.hardwareConcurrency' -sPTHREAD_POOL_SIZE_STRICT=0 -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=256MB -sMAXIMUM_MEMORY=4GB -sMAX_WEBGL_VERSION=2 -sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8','dynCall'] -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=['\$dynCall'] --bind -L${SYSROOT}/lib ${STUBS_BUILD}/libgit2_stub.a ${STUBS_BUILD}/libcurl_stub.a ${STUBS_BUILD}/libpcbnew_scripting_stub.a ${STUBS_BUILD}/libnng_stub.a ${STUBS_BUILD}/pcbnew_embind.o" \ -- -DCMAKE_PREFIX_PATH="${SYSROOT};${WX_BUILD}" \ -- -DwxWidgets_CONFIG_EXECUTABLE="${WX_BUILD}/wx-config" \ -- \ -- -DKICAD_BUILD_QA_TESTS=OFF \ -- -DKICAD_SPICE=OFF \ -- -DKICAD_USE_EGL=OFF \ -- -DKICAD_USE_BUNDLED_GLEW=ON \ -- -DKICAD_BUILD_3D_VIEWER_WASM=OFF \ -- -DKICAD_IPC_API=ON \ -- \ -- -DZSTD_ROOT="${SYSROOT}" \ -- -DZSTD_INCLUDE_DIR="${SYSROOT}/include" \ -- -DZSTD_LIBRARY="${SYSROOT}/lib/libzstd.a" \ -- -DGLM_INCLUDE_DIR="${SYSROOT}/include" \ -- -DGLM_VERSION="0.9.9.8" \ -- -DBOOST_ROOT="${SYSROOT}" \ -- -DBoost_INCLUDE_DIR="${SYSROOT}/include" \ -- -DBoost_LIBRARY_DIR="${SYSROOT}/lib" \ -- -DBoost_NO_SYSTEM_PATHS=ON \ -- -DBoost_NO_BOOST_CMAKE=ON \ -- -DFREETYPE_INCLUDE_DIR_ft2build="${SYSROOT}/include/freetype2" \ -- -DFREETYPE_INCLUDE_DIR_freetype2="${SYSROOT}/include/freetype2" \ -- -DFREETYPE_LIBRARY="${SYSROOT}/lib/libfreetype.a" \ -- -DHarfBuzz_INCLUDE_DIR="${SYSROOT}/include/harfbuzz" \ -- -DHarfBuzz_LIBRARY="${SYSROOT}/lib/libharfbuzz.a" \ -- -DOCC_INCLUDE_DIR="${SYSROOT}/include/opencascade" \ -- -DOCC_LIBRARY_DIR="${SYSROOT}/lib" \ -- -DProtobuf_INCLUDE_DIR="${SYSROOT}/include" \ -- -DProtobuf_LIBRARY="${SYSROOT}/lib/libprotobuf.a" \ -- -DProtobuf_LITE_LIBRARY="${SYSROOT}/lib/libprotobuf-lite.a" \ -- -DProtobuf_PROTOC_EXECUTABLE="${SYSROOT}/bin/protoc" \ -- -DODBC_CONFIG:STRING="stub-for-wasm" \ -- -DODBCLIB:STRING="" \ -- -DODBC_CFLAGS:STRING="" \ -- -DODBC_LINK_FLAGS:STRING="" \ -- -DODBC_LIBRARIES:STRING="" \ -- \ -- -DBUILD_GITHUB_PLUGIN=OFF \ -- -DKICAD_PCM=OFF \ -- \ -- -DHAVE_STRCASECMP=1 \ -- -DHAVE_STRNCASECMP=1 -- --# Step 7.1: Compile Embind bindings (after CMake so config.h exists) --# Exposes KiCad objects to JavaScript for future Pyodide integration --EMBIND_SRC="${PROJECT_ROOT}/wasm/bindings/pcbnew_embind.cpp" --if [ -f "$EMBIND_SRC" ]; then -- log_info "Compiling Embind bindings..." -- # Use the same includes and flags that KiCad uses -- KICAD_INCLUDES="-I${KICAD_BUILD} -I${KICAD_DIR}/include -I${KICAD_DIR}/pcbnew -I${KICAD_DIR}/common" -- KICAD_INCLUDES+=" -I${KICAD_DIR}/libs/core/include -I${KICAD_DIR}/libs/kimath/include -I${KICAD_DIR}/libs/kiplatform/include" -- KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/clipper2/Clipper2Lib/include" -- KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/nlohmann_json" -- KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/dynamic_bitset" -- KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/nanodbc" -- KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/picosha2" -- KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty" -- KICAD_INCLUDES+=" -I${SYSROOT}/include" -- # KiCad requires C++20 for concepts -- em++ -std=c++20 -c ${EXTRA_FLAGS} ${WX_CXXFLAGS} ${KICAD_INCLUDES} "$EMBIND_SRC" -o "${STUBS_BUILD}/pcbnew_embind.o" --fi -- --# Step 8: Build pcbnew target --log_info "Building pcbnew..." --emmake make -j${JOBS} pcbnew -- --# Step 8.1: Build bitmap resources (images.tar.gz) --# This creates the icon archive that KiCad loads at runtime --log_info "Building bitmap resources..." --emmake make bitmap_archive_build -- --# Step 9: Create stamp file --create_stamp "${KICAD_STAMP}" --log_info "KiCad PCBnew build complete!" --log_info "Output: ${KICAD_BUILD}/pcbnew/pcbnew.js" -+exec "${SCRIPT_DIR}/build-kicad-target.sh" pcbnew "$@" -diff --git a/tests/apps/kicad/eeschema.html b/tests/apps/kicad/eeschema.html -new file mode 100644 -index 0000000..399878d ---- /dev/null -+++ b/tests/apps/kicad/eeschema.html -@@ -0,0 +1,199 @@ -+ -+ -+ -+ -+ -+ KiCad Eeschema WASM -+ -+ -+ -+
-+ -+
-+
Initializing...
-+
-+
-+ -+
-+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/tests/kicad/eeschema.spec.ts b/tests/kicad/eeschema.spec.ts -new file mode 100644 -index 0000000..61474f3 ---- /dev/null -+++ b/tests/kicad/eeschema.spec.ts -@@ -0,0 +1,492 @@ -+import type { Page } from '@playwright/test'; -+import { test, expect } from './fixtures'; -+import { clickByLabel, clickByTooltip, findByTooltip } from '../e2e/utils/element-tracker'; -+ -+/** -+ * Eeschema (schematic editor) WASM E2E Tests -+ * -+ * Mirrors pcbnew.spec.ts. The wxWidgets setup wizard is shared infrastructure, -+ * so the wizard flow is identical. Editor-specific checks (Appearance pane, -+ * exact toolbar count, reference-image diff, etc.) are intentionally omitted -+ * here until the eeschema UI surface is empirically pinned down. -+ */ -+ -+type CanvasMetrics = { -+ dpr: number; -+ mainCanvas: null | { -+ width: number; -+ height: number; -+ rectWidth: number; -+ rectHeight: number; -+ }; -+ glCanvas: null | { -+ id: string; -+ width: number; -+ height: number; -+ rectWidth: number; -+ rectHeight: number; -+ viewport: number[] | null; -+ }; -+}; -+ -+type RegistryMetrics = { -+ elementStats: null | { -+ total: number; -+ byType: Record; -+ }; -+ renderedStats: null | { -+ total: number; -+ byType: Record; -+ }; -+ toolbars: Array<{ -+ id: string; -+ typeName: string; -+ screenX: number; -+ screenY: number; -+ width: number; -+ height: number; -+ label: string; -+ name: string; -+ }>; -+}; -+ -+type DiffRegion = { -+ x: number; -+ y: number; -+ width: number; -+ height: number; -+}; -+ -+type ScreenshotDifference = { -+ actualWidth: number; -+ actualHeight: number; -+ diffPixels: number; -+ diffRatio: number; -+ meanChannelDiff: number; -+}; -+ -+async function compareScreenshots( -+ page: Page, -+ beforePng: Buffer, -+ afterPng: Buffer, -+ region: DiffRegion -+): Promise { -+ return page.evaluate(async ({ beforeBase64, afterBase64, crop }) => { -+ const loadImage = async (base64: string): Promise => { -+ const image = new Image(); -+ image.src = `data:image/png;base64,${base64}`; -+ await image.decode(); -+ return image; -+ }; -+ -+ const [before, after] = await Promise.all([ -+ loadImage(beforeBase64), -+ loadImage(afterBase64), -+ ]); -+ -+ if (before.width !== after.width || before.height !== after.height) { -+ return { -+ actualWidth: after.width, -+ actualHeight: after.height, -+ diffPixels: Number.POSITIVE_INFINITY, -+ diffRatio: Number.POSITIVE_INFINITY, -+ meanChannelDiff: Number.POSITIVE_INFINITY, -+ }; -+ } -+ -+ const canvas = document.createElement('canvas'); -+ canvas.width = crop.width; -+ canvas.height = crop.height; -+ -+ const context = canvas.getContext('2d', { willReadFrequently: true }); -+ -+ if (!context) { -+ throw new Error('2D canvas context unavailable for screenshot comparison'); -+ } -+ -+ context.drawImage(before, crop.x, crop.y, crop.width, crop.height, 0, 0, crop.width, crop.height); -+ const beforeData = context.getImageData(0, 0, canvas.width, canvas.height).data; -+ -+ context.clearRect(0, 0, canvas.width, canvas.height); -+ context.drawImage(after, crop.x, crop.y, crop.width, crop.height, 0, 0, crop.width, crop.height); -+ const afterData = context.getImageData(0, 0, canvas.width, canvas.height).data; -+ -+ let diffPixels = 0; -+ let totalChannelDiff = 0; -+ -+ for (let i = 0; i < beforeData.length; i += 4) { -+ const dr = Math.abs(beforeData[i] - afterData[i]); -+ const dg = Math.abs(beforeData[i + 1] - afterData[i + 1]); -+ const db = Math.abs(beforeData[i + 2] - afterData[i + 2]); -+ const da = Math.abs(beforeData[i + 3] - afterData[i + 3]); -+ const maxDiff = Math.max(dr, dg, db, da); -+ -+ totalChannelDiff += dr + dg + db + da; -+ -+ if (maxDiff > 16) { -+ diffPixels += 1; -+ } -+ } -+ -+ return { -+ actualWidth: after.width, -+ actualHeight: after.height, -+ diffPixels, -+ diffRatio: diffPixels / (canvas.width * canvas.height), -+ meanChannelDiff: totalChannelDiff / beforeData.length, -+ }; -+ }, { -+ beforeBase64: beforePng.toString('base64'), -+ afterBase64: afterPng.toString('base64'), -+ crop: region, -+ }); -+} -+ -+async function getCanvasMetrics(page: Page): Promise { -+ return page.evaluate(() => { -+ const dpr = window.devicePixelRatio || 1; -+ const mainCanvas = document.querySelector('#canvas') as HTMLCanvasElement | null; -+ const glCanvas = -+ Array.from(document.querySelectorAll('[id^="glcanvas-"]')) -+ .map((canvas) => canvas as HTMLCanvasElement) -+ .find((canvas) => { -+ const rect = canvas.getBoundingClientRect(); -+ const style = window.getComputedStyle(canvas); -+ return style.display !== 'none' && rect.width > 0 && rect.height > 0; -+ }) ?? -+ document.querySelector('[id^="glcanvas-"]') as HTMLCanvasElement | null; -+ -+ const mainRect = mainCanvas?.getBoundingClientRect(); -+ const glRect = glCanvas?.getBoundingClientRect(); -+ const gl = -+ glCanvas?.getContext('webgl2') || -+ glCanvas?.getContext('webgl'); -+ const viewport = gl ? Array.from(gl.getParameter(gl.VIEWPORT) as Int32Array | number[]) : null; -+ -+ return { -+ dpr, -+ mainCanvas: mainCanvas && mainRect ? { -+ width: mainCanvas.width, -+ height: mainCanvas.height, -+ rectWidth: mainRect.width, -+ rectHeight: mainRect.height, -+ } : null, -+ glCanvas: glCanvas && glRect ? { -+ id: glCanvas.id, -+ width: glCanvas.width, -+ height: glCanvas.height, -+ rectWidth: glRect.width, -+ rectHeight: glRect.height, -+ viewport, -+ } : null, -+ }; -+ }); -+} -+ -+async function getRegistryMetrics(page: Page): Promise { -+ return page.evaluate(() => { -+ const registry = window.wxElementRegistry; -+ -+ if (!registry) { -+ return { -+ elementStats: null, -+ renderedStats: null, -+ toolbars: [], -+ }; -+ } -+ -+ const allElements = registry.findAll({ visible: true }); -+ const toolbars = allElements -+ .filter((element) => /ToolBar/.test(element.typeName)) -+ .map((element) => ({ -+ id: element.id, -+ typeName: element.typeName, -+ screenX: element.screenX, -+ screenY: element.screenY, -+ width: element.width, -+ height: element.height, -+ label: element.label, -+ name: element.name, -+ })); -+ -+ return { -+ elementStats: registry.getStats(), -+ renderedStats: registry.getRenderedStats ? registry.getRenderedStats() : null, -+ toolbars, -+ }; -+ }); -+} -+ -+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/eeschema-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/eeschema-wizard-${String(i).padStart(2, '0')}-finish.png`, -+ scale: 'device' -+ }); -+ } -+ -+ break; -+ } -+ -+ await page.waitForTimeout(500); -+ await page.screenshot({ -+ path: `test-results/eeschema-wizard-${String(i).padStart(2, '0')}.png`, -+ scale: 'device' -+ }); -+ } -+ -+ await page.waitForTimeout(2000); -+} -+ -+async function hideCursor(page: Page): Promise { -+ await page.evaluate(() => { -+ document.documentElement.style.cursor = 'none'; -+ document.body.style.cursor = 'none'; -+ }); -+} -+ -+test.describe('Eeschema WASM', () => { -+ test.beforeEach(async ({ page }) => { -+ await page.goto('/kicad/eeschema.html'); -+ }); -+ -+ test('click through setup wizard to load Eeschema', async ({ page }) => { -+ await completeWizard(page); -+ const metrics = await getCanvasMetrics(page); -+ const registryMetrics = await getRegistryMetrics(page); -+ -+ // Headless Firefox runs at dpr=1; pcbnew's stricter `> 1` check assumes a -+ // Retina-aware run. The eeschema MVP just needs to verify dpr is sane. -+ expect(metrics.dpr).toBeGreaterThanOrEqual(1); -+ expect(metrics.mainCanvas).not.toBeNull(); -+ expect(metrics.glCanvas).not.toBeNull(); -+ expect(registryMetrics.toolbars.length).toBeGreaterThanOrEqual(2); -+ -+ if (!metrics.mainCanvas || !metrics.glCanvas) { -+ throw new Error('KiCad canvases not initialized'); -+ } -+ -+ expect(Math.round(metrics.mainCanvas.rectWidth * metrics.dpr)).toBe(metrics.mainCanvas.width); -+ expect(Math.round(metrics.mainCanvas.rectHeight * metrics.dpr)).toBe(metrics.mainCanvas.height); -+ expect(metrics.glCanvas.rectWidth).toBeGreaterThan(800); -+ expect(metrics.glCanvas.rectHeight).toBeGreaterThan(500); -+ expect(Math.round(metrics.glCanvas.rectWidth * metrics.dpr)).toBe(metrics.glCanvas.width); -+ expect(Math.round(metrics.glCanvas.rectHeight * metrics.dpr)).toBe(metrics.glCanvas.height); -+ -+ const viewport = metrics.glCanvas.viewport; -+ expect(viewport).not.toBeNull(); -+ -+ if (!viewport) { -+ throw new Error('WebGL viewport unavailable'); -+ } -+ -+ expect(viewport[2]).toBe(metrics.glCanvas.width); -+ expect(viewport[3]).toBe(metrics.glCanvas.height); -+ -+ await hideCursor(page); -+ -+ // Capture a CSS-scale screenshot for visual review; no reference image -+ // is wired up yet (eeschema's chrome differs enough from pcbnew that -+ // sharing pcbnew's baseline isn't viable). Add a dedicated baseline -+ // here once the layout is finalised. -+ await page.screenshot({ -+ path: 'test-results/eeschema-loaded-css.png', -+ scale: 'css' -+ }); -+ await page.screenshot({ path: 'test-results/eeschema-loaded.png', scale: 'device' }); -+ -+ const canvasCount = await page.locator('canvas').count(); -+ expect(canvasCount).toBeGreaterThan(0); -+ }); -+ -+ test('select draw wires and draw on the schematic', async ({ page, testLogger }) => { -+ await completeWizard(page); -+ await hideCursor(page); -+ -+ await page.evaluate(() => { -+ const canvases = Array.from(document.querySelectorAll('canvas')).map((canvas) => { -+ const rect = canvas.getBoundingClientRect(); -+ const style = window.getComputedStyle(canvas); -+ return { -+ id: canvas.id, -+ className: canvas.className, -+ display: style.display, -+ visibility: style.visibility, -+ width: canvas.width, -+ height: canvas.height, -+ rectX: rect.x, -+ rectY: rect.y, -+ rectWidth: rect.width, -+ rectHeight: rect.height, -+ shouldBeVisible: (canvas as HTMLCanvasElement).dataset?.shouldBeVisible ?? null, -+ }; -+ }); -+ -+ console.log(`[TEST] canvas summary ${JSON.stringify(canvases)}`); -+ -+ const registry = window.wxElementRegistry; -+ const topLevels = (registry?.findAll?.({}) ?? []) -+ .filter((item) => /Frame|Dialog|Wizard/.test(item.typeName)) -+ .slice(0, 20) -+ .map((item) => ({ -+ id: item.id, -+ typeName: item.typeName, -+ label: item.label, -+ name: item.name, -+ visible: item.visible, -+ enabled: item.enabled, -+ screenX: item.screenX, -+ screenY: item.screenY, -+ width: item.width, -+ height: item.height, -+ })); -+ const rendered = registry?.findAllRendered?.({}) ?? []; -+ const byType = rendered.reduce>((acc, item) => { -+ acc[item.elementType] = (acc[item.elementType] ?? 0) + 1; -+ return acc; -+ }, {}); -+ const tools = rendered -+ .filter((item) => item.elementType === 'tool') -+ .slice(0, 20) -+ .map((item) => ({ -+ id: item.id, -+ label: item.label, -+ tooltip: item.tooltip, -+ checked: item.checked, -+ enabled: item.enabled, -+ })); -+ -+ console.log(`[TEST] top-level summary ${JSON.stringify(topLevels)}`); -+ console.log(`[TEST] rendered summary ${JSON.stringify({ count: rendered.length, byType, tools })}`); -+ }); -+ -+ await page.waitForFunction(() => { -+ const registry = window.wxElementRegistry; -+ if (!registry?.findAllRendered) { -+ return false; -+ } -+ -+ return registry.findAllRendered({ elementType: 'tool' }) -+ .some((tool) => tool.tooltip?.includes('Draw Wires')); -+ }, null, { timeout: 15000 }); -+ -+ const drawWiresTool = await findByTooltip(page, 'Draw Wires', { elementType: 'tool' }); -+ expect(drawWiresTool).not.toBeNull(); -+ -+ if (!drawWiresTool) { -+ throw new Error('Draw Wires tool not found in rendered element registry'); -+ } -+ -+ // The registry carries checked state via a " [checked]" label suffix -+ // appended by wxAuiToolBar::OnPaint on Emscripten — no schema change. -+ const isToolChecked = (t: { label?: string } | null | undefined) => -+ (t?.label ?? '').includes('[checked]'); -+ -+ expect(drawWiresTool.enabled).toBe(true); -+ expect(isToolChecked(drawWiresTool)).toBe(false); -+ const baselineErrorCount = testLogger.errors.length; -+ -+ await page.screenshot({ -+ path: 'test-results/eeschema-draw-wires-00-before-tool-click.png', -+ scale: 'device' -+ }); -+ -+ expect(await clickByTooltip(page, 'Draw Wires', { elementType: 'tool' })).toBe(true); -+ -+ await expect.poll(async () => { -+ const tool = await findByTooltip(page, 'Draw Wires', { elementType: 'tool' }); -+ return isToolChecked(tool); -+ }, { -+ message: 'Draw Wires tool should stay selected after the click', -+ timeout: 5000, -+ }).toBe(true); -+ -+ await page.mouse.move(640, 360); -+ await page.waitForTimeout(600); -+ -+ const selectedDrawWiresTool = await findByTooltip(page, 'Draw Wires', { elementType: 'tool' }); -+ expect(isToolChecked(selectedDrawWiresTool)).toBe(true); -+ -+ const afterToolClick = await page.screenshot({ -+ path: 'test-results/eeschema-draw-wires-01-after-click.png', -+ scale: 'device' -+ }); -+ -+ const glCanvasId = await page.evaluate(() => { -+ const glCanvas = -+ Array.from(document.querySelectorAll('[id^="glcanvas-"]')) -+ .map((canvas) => canvas as HTMLCanvasElement) -+ .find((canvas) => { -+ const rect = canvas.getBoundingClientRect(); -+ const style = window.getComputedStyle(canvas); -+ return style.display !== 'none' && rect.width > 0 && rect.height > 0; -+ }) ?? -+ document.querySelector('[id^="glcanvas-"]') as HTMLCanvasElement | null; -+ -+ return glCanvas?.id ?? null; -+ }); -+ -+ expect(glCanvasId).not.toBeNull(); -+ -+ if (!glCanvasId) { -+ throw new Error('Visible GL canvas not found'); -+ } -+ -+ const glCanvasBox = await page.locator(`#${glCanvasId}`).boundingBox(); -+ expect(glCanvasBox).not.toBeNull(); -+ -+ if (!glCanvasBox) { -+ throw new Error('GL canvas bounding box unavailable'); -+ } -+ -+ const startPoint = { -+ x: Math.round(glCanvasBox.x + glCanvasBox.width * 0.28), -+ y: Math.round(glCanvasBox.y + glCanvasBox.height * 0.36), -+ }; -+ const endPoint = { -+ x: Math.round(glCanvasBox.x + glCanvasBox.width * 0.48), -+ y: Math.round(glCanvasBox.y + glCanvasBox.height * 0.47), -+ }; -+ -+ await page.mouse.click(startPoint.x, startPoint.y); -+ await page.waitForTimeout(250); -+ await page.mouse.click(endPoint.x, endPoint.y); -+ await page.waitForTimeout(750); -+ -+ const afterDrawing = await page.screenshot({ -+ path: 'test-results/eeschema-draw-wires-02-after-drawing.png', -+ scale: 'device' -+ }); -+ -+ const diffRegion: DiffRegion = { -+ x: Math.max(0, Math.min(startPoint.x, endPoint.x) - 24), -+ y: Math.max(0, Math.min(startPoint.y, endPoint.y) - 24), -+ width: Math.abs(endPoint.x - startPoint.x) + 48, -+ height: Math.abs(endPoint.y - startPoint.y) + 48, -+ }; -+ -+ const drawingDiff = await compareScreenshots(page, afterToolClick, afterDrawing, diffRegion); -+ -+ expect(drawingDiff.diffPixels).toBeGreaterThan(120); -+ expect(drawingDiff.diffRatio).toBeGreaterThan(0.01); -+ expect(drawingDiff.meanChannelDiff).toBeGreaterThan(1); -+ -+ const realErrors = testLogger.errors -+ .slice(baselineErrorCount) -+ .filter((error) => !error.includes('favicon') && !error.includes('uncaught exception: unwind')); -+ expect(realErrors).toEqual([]); -+ }); -+}); -diff --git a/tests/package.json b/tests/package.json -index 28601d0..a58c04d 100644 ---- a/tests/package.json -+++ b/tests/package.json -@@ -13,6 +13,12 @@ - "test:kicad:chrome": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=chromium --headed", - "test:kicad": "npm run test:kicad:firefox", - "test:kicad:headed": "npm run test:kicad:chrome", -+ "test:pcbnew:firefox": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=firefox kicad/pcbnew.spec.ts", -+ "test:pcbnew:chrome": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=chromium --headed kicad/pcbnew.spec.ts", -+ "test:eeschema:firefox": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=firefox kicad/eeschema.spec.ts", -+ "test:eeschema:chrome": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=chromium --headed kicad/eeschema.spec.ts", -+ "test:eeschema": "npm run test:eeschema:firefox", -+ "test:eeschema:headed": "npm run test:eeschema:chrome", - "test:coroutine:firefox": "playwright test --config=playwright-coroutine.config.ts --project=firefox", - "test:coroutine:chrome": "playwright test --config=playwright-coroutine.config.ts --project=chromium --headed" - }, -diff --git a/tests/scripts/setup-kicad-wasm.sh b/tests/scripts/setup-kicad-wasm.sh -index 1ffaba2..ab95af0 100755 ---- a/tests/scripts/setup-kicad-wasm.sh -+++ b/tests/scripts/setup-kicad-wasm.sh -@@ -3,6 +3,8 @@ - # - # Priority: Use local output/ directory (populated by docker/build.sh) - # Fallback: Copy from Docker volume directly -+# -+# Copies whichever editors are present (pcbnew, eeschema). - - set -e - -@@ -13,32 +15,46 @@ OUTPUT_DIR="$PROJECT_ROOT/output" - - mkdir -p "$KICAD_TEST" - --# Check if output directory has the build files --if [ -f "$OUTPUT_DIR/pcbnew.js" ] && [ -f "$OUTPUT_DIR/pcbnew.wasm" ]; then -- echo "Copying KiCad WASM files from output directory..." -- cp "$OUTPUT_DIR/pcbnew.js" "$KICAD_TEST/" -- cp "$OUTPUT_DIR/pcbnew.wasm" "$KICAD_TEST/" -- # Source map for debug symbols (optional) -- cp "$OUTPUT_DIR/pcbnew.wasm.map" "$KICAD_TEST/" 2>/dev/null || true -- # Worker file for pthreads (optional) -- cp "$OUTPUT_DIR/pcbnew.worker.js" "$KICAD_TEST/" 2>/dev/null || true -- # Bitmap resources for KiCad icons (optional) -- cp "$OUTPUT_DIR/images.tar.gz" "$KICAD_TEST/" 2>/dev/null || true --else -- echo "Output directory not found, copying from Docker build..." -- docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \ -- kicad-wasm-builder:/workspace/build-wasm/kicad-pcbnew/pcbnew/pcbnew.js "$KICAD_TEST/" -- docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \ -- kicad-wasm-builder:/workspace/build-wasm/kicad-pcbnew/pcbnew/pcbnew.wasm "$KICAD_TEST/" -- # Source map for debug symbols (optional) -- docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \ -- kicad-wasm-builder:/workspace/build-wasm/kicad-pcbnew/pcbnew/pcbnew.wasm.map "$KICAD_TEST/" 2>/dev/null || true -- # Worker file for pthreads (optional) -- docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \ -- kicad-wasm-builder:/workspace/build-wasm/kicad-pcbnew/pcbnew/pcbnew.worker.js "$KICAD_TEST/" 2>/dev/null || true -- # Bitmap resources for KiCad icons (optional) -- docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \ -- kicad-wasm-builder:/workspace/build-wasm/kicad-pcbnew/resources/images.tar.gz "$KICAD_TEST/" 2>/dev/null || true -+# Copy one editor's artifacts (js, wasm, optional debug/map/worker). Returns 0 -+# if the editor was present, 1 if neither output/ nor the docker volume has it. -+copy_app() { -+ local app="$1" -+ -+ if [ -f "$OUTPUT_DIR/${app}.js" ] && [ -f "$OUTPUT_DIR/${app}.wasm" ]; then -+ echo "Copying ${app} WASM files from output directory..." -+ cp "$OUTPUT_DIR/${app}.js" "$KICAD_TEST/" -+ cp "$OUTPUT_DIR/${app}.wasm" "$KICAD_TEST/" -+ cp "$OUTPUT_DIR/${app}.wasm.map" "$KICAD_TEST/" 2>/dev/null || true -+ cp "$OUTPUT_DIR/${app}.worker.js" "$KICAD_TEST/" 2>/dev/null || true -+ cp "$OUTPUT_DIR/images.tar.gz" "$KICAD_TEST/" 2>/dev/null || true -+ return 0 -+ fi -+ -+ echo "Output ${app} not found locally, trying Docker volume..." -+ if docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \ -+ kicad-wasm-builder:/workspace/build-wasm/kicad-${app}/${app}/${app}.js "$KICAD_TEST/" 2>/dev/null \ -+ && docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \ -+ kicad-wasm-builder:/workspace/build-wasm/kicad-${app}/${app}/${app}.wasm "$KICAD_TEST/" 2>/dev/null; then -+ docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \ -+ kicad-wasm-builder:/workspace/build-wasm/kicad-${app}/${app}/${app}.wasm.map "$KICAD_TEST/" 2>/dev/null || true -+ docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \ -+ kicad-wasm-builder:/workspace/build-wasm/kicad-${app}/${app}/${app}.worker.js "$KICAD_TEST/" 2>/dev/null || true -+ docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \ -+ kicad-wasm-builder:/workspace/build-wasm/kicad-${app}/resources/images.tar.gz "$KICAD_TEST/" 2>/dev/null || true -+ return 0 -+ fi -+ -+ echo " (no ${app} artifacts found — skipping)" -+ return 1 -+} -+ -+found_any=0 -+copy_app pcbnew && found_any=1 -+copy_app eeschema && found_any=1 -+ -+if [ "$found_any" -eq 0 ]; then -+ echo "Error: neither pcbnew nor eeschema artifacts found in output/ or docker volume" >&2 -+ exit 1 - fi - - # wxWidgets WASM JavaScript glue code (defines JS functions called from WASM) -diff --git a/wasm/cmake/Findngspice.cmake b/wasm/cmake/Findngspice.cmake -index 49c0123..a275d1a 100644 ---- a/wasm/cmake/Findngspice.cmake -+++ b/wasm/cmake/Findngspice.cmake -@@ -3,14 +3,16 @@ - # We provide stub values so CMake configuration succeeds - - if(EMSCRIPTEN OR NOT KICAD_SPICE) -- message(STATUS "ngspice not available for WASM build (SPICE disabled)") -+ message(STATUS "ngspice not available for WASM build (using header stub)") - - # Set variables to indicate ngspice is "found" but disabled - set(ngspice_FOUND TRUE) - set(NGSPICE_FOUND TRUE) - -- # Provide empty values -- set(NGSPICE_INCLUDE_DIR "") -+ # Point at our header-only stub at wasm/stubs/ngspice/sharedspice.h so -+ # eeschema's sim/ngspice.{h,cpp} can compile. The library link line stays -+ # empty — the simulator frame is never instantiated in WASM. -+ set(NGSPICE_INCLUDE_DIR "${CMAKE_CURRENT_LIST_DIR}/../stubs") - set(NGSPICE_LIBRARY "") - set(NGSPICE_LIBRARIES "") - -diff --git a/wasm/stubs/char_traits_uint16_workaround.h b/wasm/stubs/char_traits_uint16_workaround.h -new file mode 100644 -index 0000000..2483938 ---- /dev/null -+++ b/wasm/stubs/char_traits_uint16_workaround.h -@@ -0,0 +1,111 @@ -+/* -+ * libc++ workaround: provide std::char_traits for WASM builds. -+ * -+ * KiCad's third-party Altium parser uses -+ * typedef std::basic_string utf16string; -+ * (kicad/thirdparty/compoundfilereader/compoundfilereader.h:264). -+ * -+ * Modern libc++ (the version bundled with current Emscripten) pulls -+ * <__format/parser_std_format_spec.h> via , which triggers implicit -+ * instantiation of char_traits. The standard only specializes -+ * char_traits for char / wchar_t / char8_t / char16_t / char32_t, so the -+ * uint16_t (== unsigned short) usage now fails to compile. -+ * -+ * We force-include this header into every translation unit via the build -+ * script's CMAKE_CXX_FLAGS so the specialization is visible before any code -+ * that needs it. Specializing std::char_traits for non-standard types is -+ * technically undefined per the standard but is the established workaround -+ * historically supported by libc++/libstdc++. -+ */ -+ -+#ifndef KICAD_WASM_CHAR_TRAITS_UINT16_WORKAROUND_H -+#define KICAD_WASM_CHAR_TRAITS_UINT16_WORKAROUND_H -+ -+#ifdef __cplusplus -+#ifdef __EMSCRIPTEN__ -+ -+#include -+#include -+#include -+#include -+ -+namespace std { -+ -+template<> -+struct char_traits -+{ -+ using char_type = unsigned short; -+ using int_type = int; -+ using off_type = streamoff; -+ using pos_type = fpos; -+ using state_type = mbstate_t; -+ -+ static constexpr void assign( char_type& a, const char_type& b ) noexcept { a = b; } -+ static constexpr bool eq( char_type a, char_type b ) noexcept { return a == b; } -+ static constexpr bool lt( char_type a, char_type b ) noexcept { return a < b; } -+ -+ static int compare( const char_type* s1, const char_type* s2, size_t n ) -+ { -+ for( size_t i = 0; i < n; ++i ) -+ { -+ if( s1[i] < s2[i] ) return -1; -+ if( s1[i] > s2[i] ) return 1; -+ } -+ return 0; -+ } -+ -+ static size_t length( const char_type* s ) -+ { -+ size_t i = 0; -+ while( s[i] != 0 ) ++i; -+ return i; -+ } -+ -+ static const char_type* find( const char_type* s, size_t n, const char_type& a ) -+ { -+ for( size_t i = 0; i < n; ++i ) -+ if( s[i] == a ) return s + i; -+ return nullptr; -+ } -+ -+ static char_type* move( char_type* s1, const char_type* s2, size_t n ) -+ { -+ return static_cast( memmove( s1, s2, n * sizeof( char_type ) ) ); -+ } -+ -+ static char_type* copy( char_type* s1, const char_type* s2, size_t n ) -+ { -+ return static_cast( memcpy( s1, s2, n * sizeof( char_type ) ) ); -+ } -+ -+ static char_type* assign( char_type* s, size_t n, char_type a ) -+ { -+ for( size_t i = 0; i < n; ++i ) s[i] = a; -+ return s; -+ } -+ -+ static constexpr int_type not_eof( int_type c ) noexcept -+ { -+ return c == eof() ? static_cast( 0 ) : c; -+ } -+ -+ static constexpr char_type to_char_type( int_type c ) noexcept -+ { -+ return static_cast( c ); -+ } -+ -+ static constexpr int_type to_int_type( char_type c ) noexcept -+ { -+ return static_cast( c ); -+ } -+ -+ static constexpr bool eq_int_type( int_type a, int_type b ) noexcept { return a == b; } -+ static constexpr int_type eof() noexcept { return static_cast( -1 ); } -+}; -+ -+} // namespace std -+ -+#endif // __EMSCRIPTEN__ -+#endif // __cplusplus -+ -+#endif // KICAD_WASM_CHAR_TRAITS_UINT16_WORKAROUND_H -diff --git a/wasm/stubs/eeschema_frame_stub.cpp b/wasm/stubs/eeschema_frame_stub.cpp -new file mode 100644 -index 0000000..1683f99 ---- /dev/null -+++ b/wasm/stubs/eeschema_frame_stub.cpp -@@ -0,0 +1,15 @@ -+/* -+ * Eeschema frame stubs for KiCad WASM build. -+ * -+ * Mirror of pcb_frame_stub.cpp. Populate as linker errors surface during the -+ * first eeschema-wasm build. Methods that need to be stubbed are typically: -+ * - Scripting helpers (LoadSchematic / SaveSchematic) when KICAD_SCRIPTING=OFF -+ * - Action-plugin glue (no plugins in WASM) -+ * - Filesystem-watcher hooks when wxUSE_FSWATCHER=0 -+ * -+ * Leave this file empty until the linker complains; the build script skips -+ * compiling it when it has zero bytes. -+ */ -+ -+#ifdef __EMSCRIPTEN__ -+#endif -diff --git a/wasm/stubs/eeschema_ngspice_data_stubs.cpp b/wasm/stubs/eeschema_ngspice_data_stubs.cpp -new file mode 100644 -index 0000000..a8e5c7c ---- /dev/null -+++ b/wasm/stubs/eeschema_ngspice_data_stubs.cpp -@@ -0,0 +1,28 @@ -+/* -+ * Empty replacements for the four largest ngspice model data initializers. -+ * -+ * Each of sim_model_ngspice_data_{bsim4,b3soi,b4soi,hsim}.cpp defines a -+ * single function (addBSIM4/addB3SOI/addB4SOI/addHSIM) that pushes hundreds -+ * of entries into NGSPICE_MODEL_INFO_MAP::modelInfos[...]. Once compiled to -+ * WASM these functions exceed the V8/SpiderMonkey limit on locals per -+ * function ("too many locals"), so Firefox refuses to instantiate the -+ * resulting module. -+ * -+ * The simulator UI is never reachable in the WASM build (FRAME_SIMULATOR -+ * fails to instantiate via the ngspice header stub at -+ * wasm/stubs/ngspice/sharedspice.h), so leaving these tables empty is safe. -+ * -+ * eeschema/CMakeLists.txt excludes the original four sources from -+ * EESCHEMA_SIM_SRCS for EMSCRIPTEN and adds this file instead. -+ */ -+ -+#ifdef __EMSCRIPTEN__ -+ -+#include -+ -+void NGSPICE_MODEL_INFO_MAP::addBSIM4() {} -+void NGSPICE_MODEL_INFO_MAP::addB3SOI() {} -+void NGSPICE_MODEL_INFO_MAP::addB4SOI() {} -+void NGSPICE_MODEL_INFO_MAP::addHSIM() {} -+ -+#endif // __EMSCRIPTEN__ -diff --git a/wasm/stubs/ngspice/sharedspice.h b/wasm/stubs/ngspice/sharedspice.h -new file mode 100644 -index 0000000..5002383 ---- /dev/null -+++ b/wasm/stubs/ngspice/sharedspice.h -@@ -0,0 +1,55 @@ -+/* -+ * Minimal stub of ngspice's sharedspice.h for KiCad WASM builds. -+ * -+ * Only the type names referenced by kicad/eeschema/sim/ngspice.{h,cpp} need -+ * to exist. The eeschema sim layer compiles but the simulator frame is never -+ * instantiated in WASM (FRAME_SIMULATOR's try/catch in IFACE::CreateKiWindow -+ * catches the init failure and returns nullptr). -+ * -+ * We intentionally do NOT define NGSPICE_PACKAGE_VERSION so that ngspice.h's -+ * fallback `typedef bool NG_BOOL;` (line 46) provides the boolean type. -+ */ -+ -+#ifndef KICAD_WASM_NGSPICE_SHAREDSPICE_STUB_H -+#define KICAD_WASM_NGSPICE_SHAREDSPICE_STUB_H -+ -+#ifdef __cplusplus -+extern "C" { -+#endif -+ -+typedef struct ngcomplex { -+ double cx_real; -+ double cx_imag; -+} ngcomplex_t; -+ -+struct vector_info { -+ char* v_name; -+ int v_type; -+ short v_flags; -+ double* v_realdata; -+ ngcomplex_t* v_compdata; -+ int v_length; -+}; -+ -+typedef struct vector_info* pvector_info; -+ -+/* Opaque payload types for callbacks we never wire up (SendData/SendInitData). */ -+typedef struct vecvaluesall* pvecvaluesall; -+typedef struct vecinfoall* pvecinfoall; -+ -+/* -+ * Function types (not pointers). ngspice.h references them as `SendChar*` etc., -+ * so the trailing star in the typedef site makes the pointer. -+ */ -+typedef int (SendChar)(char*, int, void*); -+typedef int (SendStat)(char*, int, void*); -+typedef int (ControlledExit)(int, bool, bool, int, void*); -+typedef int (SendData)(pvecvaluesall, int, int, void*); -+typedef int (SendInitData)(pvecinfoall, int, void*); -+typedef int (BGThreadRunning)(bool, int, void*); -+ -+#ifdef __cplusplus -+} -+#endif -+ -+#endif /* KICAD_WASM_NGSPICE_SHAREDSPICE_STUB_H */ diff --git a/scripts/common/apply-asyncify.sh b/scripts/common/apply-asyncify.sh index 15d328c..2007163 100755 --- a/scripts/common/apply-asyncify.sh +++ b/scripts/common/apply-asyncify.sh @@ -65,7 +65,7 @@ echo "" echo "Running wasm-opt -O2 on the asyncified wasm..." echo " Purpose: shrink asyncify-instrumented functions back under V8's" echo " per-function locals limit (otherwise large coroutine-entry and" -echo " similar functions silently stall in Chrome's V8). See DEBUG.md §7" +echo " similar functions silently stall in Chrome's V8). See docs/debugging/DEBUG.md §7" echo " and memory/bundle-size-asyncify-optimization.md." echo " This pass also takes several minutes and ~10-15 GB RAM." diff --git a/web/README.md b/web/README.md index 43d7afd..b4597af 100644 --- a/web/README.md +++ b/web/README.md @@ -7,7 +7,7 @@ WASM tools (pcbnew / eeschema / calculator) by URL: /p/// e.g. /p/project5/pcbnew/nyak.kicad_pcb ``` -Design + decisions: [`../features/web-init/0001-web-app-spec.md`](../features/web-init/0001-web-app-spec.md). +Design + decisions: [`../docs/features/web-init/0001-web-app-spec.md`](../docs/features/web-init/0001-web-app-spec.md). ## Stack