docs: ✏️ cleanup and organize docs

This commit is contained in:
Istvan Matejcsok 2026-06-05 12:15:54 +02:00
commit 8e413f89ec
29 changed files with 85 additions and 3340 deletions

4
.gitignore vendored
View file

@ -63,3 +63,7 @@ output/
.playwright-mcp .playwright-mcp
.claude/worktrees/ .claude/worktrees/
# Feature patch scratch (generated by scripts/create-feature-patches.sh).
# Curated design docs live under docs/features/ and ARE committed.
/features/

View file

@ -64,13 +64,12 @@ kicad-wasm/
## Feature Branches ## Feature Branches
Each feature branch has a folder in `features/<branch-name>/` containing: Curated design docs and research notes for each feature live in
- Documentation and research notes [`docs/features/<branch-name>/`](docs/features/) (committed).
- `root.patch` - changes to main repo
- `kicad.patch` - changes to KiCad fork
- `wxwidgets.patch` - changes to wxWidgets fork
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/<branch-name>/`
scratch dir. That dir is gitignored — the patches are local history, not committed.
## Two Build Workflows ## Two Build Workflows
@ -91,7 +90,7 @@ cd tests && npm install && npm run test:kicad
Output: `output/pcbnew.js`, `output/pcbnew.wasm` 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) ### 2. wxWidgets Test Apps (Local)
@ -152,8 +151,11 @@ See [tests/README.md](tests/README.md) for test documentation.
## 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 - [Docker README](docker/README.md) - Container setup
- [Debugging Guide](docs/debugging/DEBUG.md) - Asyncify/WASM debugging
- [Tests README](tests/README.md) - Test infrastructure - [Tests README](tests/README.md) - Test infrastructure
## License ## License

58
docs/README.md Normal file
View file

@ -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)

View file

@ -1,5 +1,7 @@
# KiCad IPC API - WASM Fork Changes # 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 ## Overview
We added `#ifdef KICAD_IPC_API` guards to 18 KiCad source files. These guards wrap: We added `#ifdef KICAD_IPC_API` guards to 18 KiCad source files. These guards wrap:

View file

@ -1,5 +1,7 @@
# OpenGL, GAL, and WebGL Strategy for KiCad WASM # 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 ## Summary
This document analyzes KiCad's graphics architecture and evaluates strategies for WebGL rendering in the WASM build. This document analyzes KiCad's graphics architecture and evaluates strategies for WebGL rendering in the WASM build.

View file

@ -1,5 +1,7 @@
# GAL Native Test Harness Architecture # 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 ## 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. 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.

View file

@ -1,5 +1,7 @@
# WebGL GAL Port - Implementation Complete # 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 ✅ ## Status: ALL PHASES COMPLETE ✅
**Branch:** `webgl` (21+ commits) **Branch:** `webgl` (21+ commits)

View file

@ -2,7 +2,7 @@
## Context ## 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: But tools still don't work end-to-end:

View file

@ -34,7 +34,7 @@ verbatim.
mirroring the eeschema/pcbnew static-link pattern. mirroring the eeschema/pcbnew static-link pattern.
- **`kicad/eeschema/eeschema.cpp`** — `IFACE::CreateKiWindow`'s `FRAME_SCH_SYMBOL_EDITOR` - **`kicad/eeschema/eeschema.cpp`** — `IFACE::CreateKiWindow`'s `FRAME_SCH_SYMBOL_EDITOR`
case was stubbed to `return nullptr` on `__EMSCRIPTEN__` during the eeschema MVP 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 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` 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). bailed, and the app sat idle with a blank canvas (no abort, no error).

View file

@ -148,7 +148,7 @@ The `emscripten_fiber_swap` mechanism handles this correctly because the fiber J
### The `setTimeout(wakeUp, 0)` Pattern ### 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) **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)

View file

@ -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
$<$<BOOL:${KICAD_MAKE_LINK_MAPS}>:-Wl,--cref,-Map=pl_editor.map>
)
-# the main pl_editor program, in DSO form.
-add_library( pl_editor_kiface MODULE
+# the main pl_editor program, in OBJECT form so it can be statically linked on WASM.
+add_library( pl_editor_kiface_objects OBJECT
pl_editor.cpp
${PL_EDITOR_SRCS}
${DIALOGS_SRCS}
${PL_EDITOR_EXTRA_SRCS}
)
-target_link_libraries( pl_editor_kiface
- gal
- common
- core
- ${wxWidgets_LIBRARIES}
+
+target_link_libraries( pl_editor_kiface_objects
+ PRIVATE
+ common
+ core
+ ${wxWidgets_LIBRARIES}
)
+
+add_library( pl_editor_kiface MODULE )
+
set_target_properties( pl_editor_kiface PROPERTIES
OUTPUT_NAME pl_editor
PREFIX ${KIFACE_PREFIX}
SUFFIX ${KIFACE_SUFFIX}
)
-set_source_files_properties( pl_editor.cpp PROPERTIES
- # The KIFACE is in pcbnew.cpp, export it:
- COMPILE_DEFINITIONS "BUILD_KIWAY_DLL;COMPILING_DLL"
- )
+if( EMSCRIPTEN )
+ # WASM: Static linking - don't define BUILD_KIWAY_DLL
+ set_source_files_properties( pl_editor.cpp PROPERTIES
+ COMPILE_DEFINITIONS "COMPILING_DLL"
+ )
+else()
+ set_source_files_properties( pl_editor.cpp PROPERTIES
+ # The KIFACE is in pl_editor.cpp, export it:
+ COMPILE_DEFINITIONS "BUILD_KIWAY_DLL;COMPILING_DLL"
+ )
+endif()
target_link_options( pl_editor_kiface PRIVATE
$<$<BOOL:${KICAD_MAKE_LINK_MAPS}>:-Wl,--cref,-Map=_pl_editor.kiface.map>
)
-# if building pl_editor, then also build pl_editor_kiface if out of date.
-add_dependencies( pl_editor pl_editor_kiface )
-
message( STATUS "Including 3Dconnexion SpaceMouse navigation support in pagelayout editor" )
add_subdirectory( navlib )
-target_link_libraries( pl_editor_kiface pl_editor_navlib)
+set( PL_EDITOR_KIFACE_LIBRARIES
+ pl_editor_kiface_objects
+ pl_editor_navlib
+ kicommon
+ kiplatform
+ common
+ gal
+ core
+ ${wxWidgets_LIBRARIES}
+ )
+
+# WASM: Link kiface objects directly into pl_editor executable (static linking)
+if( EMSCRIPTEN )
+ target_link_libraries( pl_editor
+ PRIVATE
+ ${PL_EDITOR_KIFACE_LIBRARIES}
+ )
+ target_link_options( pl_editor PRIVATE
+ "LINKER:--allow-multiple-definition"
+ )
+endif()
+
+target_link_libraries( pl_editor_kiface
+ PRIVATE
+ ${PL_EDITOR_KIFACE_LIBRARIES}
+ )
+
+# if building pl_editor, then also build pl_editor_kiface if out of date.
+add_dependencies( pl_editor pl_editor_kiface )
-add_dependencies( pl_editor_kiface pl_editor_navlib)
+add_dependencies( pl_editor_kiface pl_editor_navlib )
# these 2 binaries are a matched set, keep them together:
if( APPLE )
diff --git a/pagelayout_editor/navlib/CMakeLists.txt b/pagelayout_editor/navlib/CMakeLists.txt
index ad3c388736..89e94e6abb 100644
--- a/pagelayout_editor/navlib/CMakeLists.txt
+++ b/pagelayout_editor/navlib/CMakeLists.txt
@@ -1,25 +1,36 @@
-add_library(pl_editor_navlib STATIC
- "nl_pl_editor_plugin.cpp"
- "nl_pl_editor_plugin_impl.cpp"
-)
+# WASM: 3D mouse support not available, use stubs
+if( EMSCRIPTEN )
+ add_library(pl_editor_navlib STATIC
+ "${CMAKE_SOURCE_DIR}/../wasm/stubs/nl_pl_editor_plugin_stub.cpp"
+ )
+ target_include_directories(pl_editor_navlib PRIVATE
+ ${CMAKE_SOURCE_DIR}/pagelayout_editor
+ ${CMAKE_SOURCE_DIR}/include
+ )
+else()
+ add_library(pl_editor_navlib STATIC
+ "nl_pl_editor_plugin.cpp"
+ "nl_pl_editor_plugin_impl.cpp"
+ )
-# pl_editor_navlib depends on make_lexer outputs in common
-add_dependencies( pl_editor_navlib common )
+ # pl_editor_navlib depends on make_lexer outputs in common
+ add_dependencies( pl_editor_navlib common )
-# Find the 3DxWare SDK component 3DxWare::NlClient
-# find_package(TDxWare_SDK 4.0 REQUIRED COMPONENTS 3DxWare::Navlib)
-target_compile_definitions(pl_editor_navlib PRIVATE
- $<TARGET_PROPERTY:3DxWare::Navlib,INTERFACE_COMPILE_DEFINITIONS>
-)
-target_compile_options(pl_editor_navlib PRIVATE
- $<TARGET_PROPERTY:3DxWare::Navlib,INTERFACE_COMPILE_OPTIONS>
-)
-target_include_directories(pl_editor_navlib PRIVATE
- $<TARGET_PROPERTY:3DxWare::Navlib,INTERFACE_INCLUDE_DIRECTORIES>
- $<TARGET_PROPERTY:pl_editor_kiface,INCLUDE_DIRECTORIES>
-)
-target_link_libraries(pl_editor_navlib
- $<TARGET_PROPERTY:3DxWare::Navlib,INTERFACE_LINK_LIBRARIES>
- 3DxWare::Navlib
-)
+ # Find the 3DxWare SDK component 3DxWare::NlClient
+ # find_package(TDxWare_SDK 4.0 REQUIRED COMPONENTS 3DxWare::Navlib)
+ target_compile_definitions(pl_editor_navlib PRIVATE
+ $<TARGET_PROPERTY:3DxWare::Navlib,INTERFACE_COMPILE_DEFINITIONS>
+ )
+ target_compile_options(pl_editor_navlib PRIVATE
+ $<TARGET_PROPERTY:3DxWare::Navlib,INTERFACE_COMPILE_OPTIONS>
+ )
+ target_include_directories(pl_editor_navlib PRIVATE
+ $<TARGET_PROPERTY:3DxWare::Navlib,INTERFACE_INCLUDE_DIRECTORIES>
+ $<TARGET_PROPERTY:pl_editor_kiface,INCLUDE_DIRECTORIES>
+ )
+ target_link_libraries(pl_editor_navlib
+ $<TARGET_PROPERTY:3DxWare::Navlib,INTERFACE_LINK_LIBRARIES>
+ 3DxWare::Navlib
+ )
+endif()

View file

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

View file

@ -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 <emscripten/html5.h>
-//#define HAS_MOUSE_DETAIL
+// Double-click detection threshold (ms). Matches the default
+// wxSYS_DCLICK_MSEC on most platforms.
+#define WASM_DCLICK_MSEC 500.0
namespace
{
@@ -63,11 +65,32 @@ wxEventType GetMouseEventType(int emscriptenEventType,
wxEventType eventType;
std::string eventName;
-#ifdef HAS_MOUSE_DETAIL
- int clickCount = event.detail;
-#else
+ // EmscriptenMouseEvent no longer exposes a click-count field, so we
+ // detect double-clicks ourselves: two MOUSEDOWNs of the same button
+ // within WASM_DCLICK_MSEC count as a double-click. The browser also
+ // dispatches a real 'dblclick' event we could hook, but tracking it
+ // on MOUSEDOWN lets wxEVT_LEFT_DCLICK arrive at the same point in the
+ // sequence as on desktop (between LEFT_DOWN and LEFT_UP), which is
+ // what wxGenericListCtrl's activation logic expects.
+ static double lastMouseDownTime = 0.0;
+ static unsigned short lastMouseDownButton = 0xFFFF;
int clickCount = 1;
-#endif
+ if (emscriptenEventType == EMSCRIPTEN_EVENT_MOUSEDOWN)
+ {
+ if (event.button == lastMouseDownButton &&
+ (event.timestamp - lastMouseDownTime) < WASM_DCLICK_MSEC)
+ {
+ clickCount = 2;
+ // Reset so a quick third click isn't chained as another DCLICK.
+ lastMouseDownTime = 0.0;
+ lastMouseDownButton = 0xFFFF;
+ }
+ else
+ {
+ lastMouseDownTime = event.timestamp;
+ lastMouseDownButton = event.button;
+ }
+ }
switch (emscriptenEventType)
{

View file

@ -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<unsigned short>
+# 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<wxString> 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<wxFileSystemWatcher> 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 <core/profile.h>
#include <project/project_file.h>
#include <project/net_settings.h>
+#ifdef KICAD_SCRIPTING
#include <python_scripting.h>
+#endif
#include <sch_edit_frame.h>
#include <symbol_chooser_frame.h>
#include <sch_painter.h>
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 <wx/uri.h>
#include <sch_io/sch_io_mgr.h>
-#include <sch_io/eagle/sch_io_eagle.h>
#include <sch_io/kicad_legacy/sch_io_kicad_legacy.h>
#include <sch_io/kicad_sexpr/sch_io_kicad_sexpr.h>
+#include <sch_io/http_lib/sch_io_http_lib.h>
+// 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 <sch_io/eagle/sch_io_eagle.h>
#include <sch_io/altium/sch_io_altium.h>
#include <sch_io/cadstar/sch_io_cadstar_archive.h>
#include <sch_io/easyeda/sch_io_easyeda.h>
#include <sch_io/easyedapro/sch_io_easyedapro.h>
#include <sch_io/database/sch_io_database.h>
#include <sch_io/ltspice/sch_io_ltspice.h>
-#include <sch_io/http_lib/sch_io_http_lib.h>
+#endif
#include <common.h> // for ExpandEnvVarSubstitutions
#include <wildcards_and_files_ext.h>
@@ -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 <bitmaps.h>
#include <eeschema_id.h>
#include <pgm_base.h>
+#ifdef KICAD_SCRIPTING
#include <python_scripting.h>
+#endif
#include <tool/tool_manager.h>
#include <tool/action_toolbar.h>
#include <tools/sch_actions.h>
@@ -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 &&

File diff suppressed because it is too large Load diff

View file

@ -65,7 +65,7 @@ echo ""
echo "Running wasm-opt -O2 on the asyncified wasm..." echo "Running wasm-opt -O2 on the asyncified wasm..."
echo " Purpose: shrink asyncify-instrumented functions back under V8's" echo " Purpose: shrink asyncify-instrumented functions back under V8's"
echo " per-function locals limit (otherwise large coroutine-entry and" 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 " and memory/bundle-size-asyncify-optimization.md."
echo " This pass also takes several minutes and ~10-15 GB RAM." echo " This pass also takes several minutes and ~10-15 GB RAM."

View file

@ -7,7 +7,7 @@ WASM tools (pcbnew / eeschema / calculator) by URL:
/p/<project>/<tool>/<file-path> e.g. /p/project5/pcbnew/nyak.kicad_pcb /p/<project>/<tool>/<file-path> 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 ## Stack