From 730147d6908cb59fe353116ab3306f2011590fd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20T=C3=B6rcsv=C3=A1ri?= Date: Fri, 29 May 2026 06:39:23 +0200 Subject: [PATCH] feat(schematic): eeschema WASM build + e2e harness --- docker/build.sh | 88 +- .../schematic/0001-eeschema-iface-stubs.md | 54 + features/schematic/kicad.patch | 432 ++++ features/schematic/root.patch | 2005 +++++++++++++++++ kicad | 2 +- scripts/create-feature-patches.sh | 34 +- scripts/kicad/build-eeschema.sh | 7 + scripts/kicad/build-kicad-target.sh | 395 ++++ scripts/kicad/build-pcbnew.sh | 348 +-- tests/apps/kicad/eeschema.html | 199 ++ tests/kicad/eeschema.spec.ts | 492 ++++ tests/package.json | 8 +- tests/scripts/setup-kicad-wasm.sh | 68 +- wasm/cmake/Findngspice.cmake | 8 +- wasm/stubs/char_traits_uint16_workaround.h | 111 + wasm/stubs/eeschema_frame_stub.cpp | 15 + wasm/stubs/eeschema_ngspice_data_stubs.cpp | 28 + wasm/stubs/ngspice/sharedspice.h | 55 + wxwidgets | 2 +- 19 files changed, 3941 insertions(+), 410 deletions(-) create mode 100644 features/schematic/0001-eeschema-iface-stubs.md create mode 100644 features/schematic/kicad.patch create mode 100644 features/schematic/root.patch create mode 100755 scripts/kicad/build-eeschema.sh create mode 100755 scripts/kicad/build-kicad-target.sh create mode 100644 tests/apps/kicad/eeschema.html create mode 100644 tests/kicad/eeschema.spec.ts create mode 100644 wasm/stubs/char_traits_uint16_workaround.h create mode 100644 wasm/stubs/eeschema_frame_stub.cpp create mode 100644 wasm/stubs/eeschema_ngspice_data_stubs.cpp create mode 100644 wasm/stubs/ngspice/sharedspice.h diff --git a/docker/build.sh b/docker/build.sh index d466557..4e8a8c0 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=("$@") @@ -62,34 +87,45 @@ if [ $sync_rc -ne 0 ] && [ $sync_rc -ne 24 ]; then echo "ERROR: source sync failed after retries (exit ${sync_rc})"; exit 1 fi -# Run build command (without asyncify - handled on host due to memory requirements) -# -e EMSDK=/emsdk: `docker compose exec` bypasses the entrypoint that sources +# Build one app: compile in container, then run host-side post-processing. +build_app() { + local app="$1" + echo "" + echo "=== Building ${app} ===" + + # Run build command (without asyncify - handled on host due to memory requirements) + # -e EMSDK=/emsdk: `docker compose exec` bypasses the entrypoint that sources # emsdk_env.sh, so the build shell would lack emcc/embuilder on PATH. Setting # EMSDK lets scripts/common/env.sh source /emsdk/emsdk_env.sh and activate the toolchain. -docker compose -f docker/docker-compose.yml exec -e EMSDK=/emsdk kicad-wasm-builder \ - /workspace/scripts/kicad/build-pcbnew.sh "${ARGS[@]}" +docker compose -f docker/docker-compose.yml exec -e EMSDK=/emsdkkicad-wasm-builder \ + "/workspace/scripts/kicad/build-${app}.sh" "${ARGS[@]}" -# 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" + # 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 into pcbnew.js -# This fixes "dynCall_* is not defined" errors in Emscripten 4.x -./scripts/common/inject-dyncall-shims.sh output/pcbnew.js + # 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) -# 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 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 -# This is done on the host because wasm-opt --asyncify needs significant RAM -./scripts/common/apply-asyncify.sh output/pcbnew.wasm output/pcbnew.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/features/schematic/0001-eeschema-iface-stubs.md b/features/schematic/0001-eeschema-iface-stubs.md new file mode 100644 index 0000000..29d9a7c --- /dev/null +++ b/features/schematic/0001-eeschema-iface-stubs.md @@ -0,0 +1,54 @@ +# 0001 — eeschema IFACE sub-frame stubs (WASM) + +## Why + +Eeschema's `IFACE::CreateKiWindow` in [kicad/eeschema/eeschema.cpp](../../kicad/eeschema/eeschema.cpp) dispatches `FRAME_T` ids to four frame constructors: + +- `FRAME_SCH` → `SCH_EDIT_FRAME` (the schematic editor — what we want) +- `FRAME_SCH_SYMBOL_EDITOR` → `SYMBOL_EDIT_FRAME` (symbol library editor) +- `FRAME_SCH_VIEWER` → `SYMBOL_VIEWER_FRAME` (symbol library viewer) +- `FRAME_SYMBOL_CHOOSER` → `SYMBOL_CHOOSER_FRAME` (symbol chooser dialog) +- `FRAME_SIMULATOR` → `SIMULATOR_FRAME` (already wrapped in try/catch — no patch needed) + +The MVP scope for the WASM port is "empty schematic editor + draw a wire", so the three sub-frames (symbol editor / viewer / chooser) are out of scope. They are non-trivial to support in the browser — they need bundled symbol libraries, FS access for `.kicad_sym` lookup, and full dialog plumbing. + +Compiling them out at the source level (removing `EESCHEMA_LIBEDIT_SRCS` from the build) cascades into many CMakeLists.txt and symbol-chooser sources; cleaner to keep the sources compiled and just refuse to instantiate the frames at the IFACE switch. + +## What changed + +`kicad/eeschema/eeschema.cpp`, the three sub-frame cases inside `IFACE::CreateKiWindow` are now guarded: + +```cpp +case FRAME_SCH_SYMBOL_EDITOR: +#ifdef __EMSCRIPTEN__ + return nullptr; +#else + return new SYMBOL_EDIT_FRAME( aKiway, aParent ); +#endif + +case FRAME_SCH_VIEWER: +#ifdef __EMSCRIPTEN__ + return nullptr; +#else + return new SYMBOL_VIEWER_FRAME( aKiway, aParent ); +#endif + +case FRAME_SYMBOL_CHOOSER: +{ +#ifdef __EMSCRIPTEN__ + return nullptr; +#else + // … existing body … +#endif +} +``` + +`FRAME_SIMULATOR` is untouched — its existing `try/catch (SIMULATOR_INIT_ERR&)` block catches the ngspice init failure that our header stub eventually triggers, and returns nullptr the same way. + +## How to apply + +Captured under `features/schematic/kicad.patch` via `./scripts/create-feature-patches.sh schematic` once the build is green. + +## Tested by + +`tests/kicad/eeschema.spec.ts` — wizard completion + `Draw Wires` tool test. Neither test path exercises the stubbed-out frames. diff --git a/features/schematic/kicad.patch b/features/schematic/kicad.patch new file mode 100644 index 0000000..1346101 --- /dev/null +++ b/features/schematic/kicad.patch @@ -0,0 +1,432 @@ +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 new file mode 100644 index 0000000..56584c3 --- /dev/null +++ b/features/schematic/root.patch @@ -0,0 +1,2005 @@ +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/kicad b/kicad index 07d8130..4cad41a 160000 --- a/kicad +++ b/kicad @@ -1 +1 @@ -Subproject commit 07d8130d44493fc949da3611d50e0c3b1652f930 +Subproject commit 4cad41af0665d166f9da4c69ae1e42d5a043538f 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 68f6e55..c1a7d9a 100644 --- a/tests/package.json +++ b/tests/package.json @@ -9,15 +9,21 @@ "build-wasm": "cd apps && make -f Makefile.wasm", "serve": "npx serve apps -p 8080 -c ../serve.json", "setup:kicad": "./scripts/setup-kicad-wasm.sh", + "setup:calculator": "./scripts/setup-calculator-wasm.sh", "test:kicad:firefox": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=firefox", "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", - "setup:calculator": "./scripts/setup-calculator-wasm.sh", + "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:calculator:firefox": "npm run setup:calculator && playwright test --config=playwright-calculator.config.ts --project=firefox", "test:calculator:chrome": "npm run setup:calculator && playwright test --config=playwright-calculator.config.ts --project=chromium --headed", "test:calculator": "npm run test:calculator:firefox", "test:calculator:headed": "npm run test:calculator:chrome", + "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/wxwidgets b/wxwidgets index 6fb2eac..d1d1627 160000 --- a/wxwidgets +++ b/wxwidgets @@ -1 +1 @@ -Subproject commit 6fb2eac2572cf0d3964ba8bec8d73e017d311733 +Subproject commit d1d1627b279672fc71deb4ff4512a7771dfc2cc8