diff --git a/docker/build.sh b/docker/build.sh index e4b01bf..a9be723 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -6,10 +6,12 @@ # ./docker/build.sh [args...] # # Apps: -# pcbnew PCB editor -# eeschema schematic editor -# calculator PCB calculator -# all build pcbnew, eeschema, calculator sequentially +# pcbnew PCB editor +# eeschema schematic editor +# calculator PCB calculator +# pl_editor drawing-sheet editor +# symbol_editor symbol editor (eeschema kiface, FRAME_SCH_SYMBOL_EDITOR) +# all build all of the above sequentially # # Any extra args are forwarded to scripts/kicad/build-.sh (e.g. -j 8, # --full, --release, --diag=gal). @@ -46,7 +48,7 @@ trap 'kw_fail 130; exit 130' INT TERM cd "$(dirname "$0")/.." -VALID_APPS="pcbnew | eeschema | calculator | pl_editor | all" +VALID_APPS="pcbnew | eeschema | calculator | pl_editor | symbol_editor | all" usage() { echo "Usage: ./docker/build.sh [args...]" >&2 @@ -69,7 +71,7 @@ APP_NAME="$1" shift case "$APP_NAME" in - pcbnew|eeschema|calculator|pl_editor|all) ;; + pcbnew|eeschema|calculator|pl_editor|symbol_editor|all) ;; *) echo "Error: unknown app '$APP_NAME' (expected: ${VALID_APPS})" >&2 usage @@ -132,9 +134,10 @@ fi # but lives under the pcb_calculator/ subtree. kicad_subdir_for() { case "$1" in - calculator) echo "pcb_calculator" ;; - pl_editor) echo "pagelayout_editor" ;; - *) echo "$1" ;; + calculator) echo "pcb_calculator" ;; + pl_editor) echo "pagelayout_editor" ;; + symbol_editor) echo "eeschema" ;; + *) echo "$1" ;; esac } @@ -182,10 +185,11 @@ build_app() { } if [[ "${APP_NAME}" == "all" ]]; then - build_app pcbnew 1 4 - build_app eeschema 2 4 - build_app calculator 3 4 - build_app pl_editor 4 4 + build_app pcbnew 1 5 + build_app eeschema 2 5 + build_app calculator 3 5 + build_app pl_editor 4 5 + build_app symbol_editor 5 5 else build_app "${APP_NAME}" 1 1 fi diff --git a/features/symbol-editor/0001-symbol-editor-port.md b/features/symbol-editor/0001-symbol-editor-port.md new file mode 100644 index 0000000..577948c --- /dev/null +++ b/features/symbol-editor/0001-symbol-editor-port.md @@ -0,0 +1,85 @@ +# Symbol Editor WASM port — design notes + +## Goal + +Bring up KiCad's Symbol Editor (`FRAME_SCH_SYMBOL_EDITOR`, the `.kicad_sym` +library editor) in the browser, to the same "boots, canvas visible, click +around" level as the other ported apps. Scope is launch-only — library +load/save and the symbol viewer/chooser sub-frames are out of scope for now. + +## Key insight + +Unlike pcbnew / pl_editor, the symbol editor is **not a separate program**. It is +served by the **eeschema kiface**: its sources already compile into +`eeschema_kiface_objects` (`EESCHEMA_LIBEDIT_SRCS` + the `symbol_editor_*` tools in +`kicad/eeschema/CMakeLists.txt`). KiCad's universal launcher `common/single_top.cpp` +opens whichever frame the compile-time `TOP_FRAME` macro names. + +So the port is just **a second launcher executable (`symbol_editor`) that links the +same eeschema kiface but compiles `single_top.cpp` with +`TOP_FRAME=FRAME_SCH_SYMBOL_EDITOR`** — no new sources, no new kiface, no extracting +symbol-editor code. The eeschema kiface (deps, navlib, stubs, libraries) is reused +verbatim. + +## Changes + +### kicad submodule (2 files) + +- **`kicad/eeschema/CMakeLists.txt`** — a WASM-only (`if( EMSCRIPTEN )`) block adds the + `symbol_editor` executable. Because `single_top.cpp`'s `COMPILE_DEFINITIONS` are + directory-scoped (already pinned to `TOP_FRAME=FRAME_SCH` for the `eeschema` exe), + we `configure_file`-copy it to a private TU (`symbol_editor_single_top.cpp`) and set + that copy's `TOP_FRAME=FRAME_SCH_SYMBOL_EDITOR;PGM_DATA_FILE_EXT="kicad_sym"`. The exe + links `EESCHEMA_KIFACE_LIBRARIES` directly with `LINKER:--allow-multiple-definition`, + mirroring the eeschema/pcbnew static-link pattern. +- **`kicad/eeschema/eeschema.cpp`** — `IFACE::CreateKiWindow`'s `FRAME_SCH_SYMBOL_EDITOR` + case was stubbed to `return nullptr` on `__EMSCRIPTEN__` during the eeschema MVP + (see `features/schematic/0001-eeschema-iface-stubs.md`). That stub is now removed so + the frame is constructed in WASM like the native build. This was THE blocker: with the + stub, `Kiway.Player(FRAME_SCH_SYMBOL_EDITOR)` returned null, `single_top`'s `OnInit` + bailed, and the app sat idle with a blank canvas (no abort, no error). + + The symbol viewer (`FRAME_SCH_VIEWER`) and chooser (`FRAME_SYMBOL_CHOOSER`) remain + stubbed — out of scope, and the chooser needs bundled libraries we don't ship. + +### Root repo + +- **`scripts/kicad/build-symbol_editor.sh`** — thin wrapper around + `build-kicad-target.sh symbol_editor`. +- **`scripts/kicad/build-kicad-target.sh`** — adds `symbol_editor` to the `case`. CMake + target is `symbol_editor` but its build subdir is `eeschema` (it's part of that + kiface), so a `KICAD_SUBDIR` variable now distinguishes target name from subdir for + the output-path log and embind include. +- **`docker/build.sh`** — adds `symbol_editor` to valid apps, the `all` loop, and + `kicad_subdir_for` (`symbol_editor → eeschema`); artifacts land at + `build-wasm/kicad-symbol_editor/eeschema/symbol_editor.{js,wasm}`. +- **`tests/apps/kicad/symbol_editor.html`** — browser shell (copy of eeschema.html with + title + `thisProgram=/usr/bin/symbol_editor` + `symbol_editor.js`). +- **`tests/scripts/setup-kicad-wasm.sh`** — `copy_app symbol_editor` + subdir map entry. +- **`tests/kicad/symbol_editor.spec.ts`** + **`tests/package.json`** — launch-scope smoke + test (canvas visible, registry populated, toolbars present, no WASM abort), mirroring + eeschema's wizard-aware flow. + +### wxwidgets submodule + +No changes needed — the file-dialog and double-click fixes landed with the pl_editor port. + +## Build & verify + +``` +./docker/build.sh symbol_editor +cd tests && npm run setup:kicad && npm run test:symbol_editor +# or serve tests/apps/kicad and open symbol_editor.html +``` + +Expect: the symbol editor window opens — menu bar, top + left + right toolbars +(incl. pin/rect/circle/line drawing tools), the symbol library tree pane with the +filter box, the gridded canvas with the symbol-origin crosshair, and a status bar. + +## Known limitations + +- No bundled symbol libraries, so the library tree is empty (`SyncLibraries` reports + `libCount=0`). Opening/creating/saving `.kicad_sym` files is untested (out of scope). +- Symbol viewer and symbol chooser frames are still stubbed out for WASM. +- No persistent storage (MEMFS only). + diff --git a/kicad b/kicad index 881ab17..211ab47 160000 --- a/kicad +++ b/kicad @@ -1 +1 @@ -Subproject commit 881ab171814da6e73e116f91e9e18fb4dd9fcf6b +Subproject commit 211ab47df18abe03af3c8c1c8cfbbe5a42520129 diff --git a/scripts/kicad/build-kicad-target.sh b/scripts/kicad/build-kicad-target.sh index 0033640..27feb61 100755 --- a/scripts/kicad/build-kicad-target.sh +++ b/scripts/kicad/build-kicad-target.sh @@ -31,21 +31,37 @@ set -e if [ -z "$1" ]; then - echo "Error: missing argument (pcbnew | eeschema | calculator | pl_editor)" >&2 + echo "Error: missing argument (pcbnew | eeschema | calculator | pl_editor | symbol_editor)" >&2 exit 1 fi APP_NAME="$1" shift +# KICAD_TARGET: the CMake/make target name. +# KICAD_SUBDIR: the source/build subdirectory the target's artifacts land in. +# Most apps share all three names; the exceptions: +# - calculator: target+subdir are both pcb_calculator (OUTPUT_NAME=calculator) +# - pl_editor: subdir is pagelayout_editor (upstream source dir name) +# - symbol_editor: served by the eeschema kiface, so it builds in eeschema/ case "$APP_NAME" in - pcbnew|eeschema|pl_editor) + pcbnew|eeschema) KICAD_TARGET="$APP_NAME" + KICAD_SUBDIR="$APP_NAME" + ;; + pl_editor) + KICAD_TARGET="pl_editor" + KICAD_SUBDIR="pagelayout_editor" ;; calculator) KICAD_TARGET="pcb_calculator" + KICAD_SUBDIR="pcb_calculator" + ;; + symbol_editor) + KICAD_TARGET="symbol_editor" + KICAD_SUBDIR="eeschema" ;; *) - echo "Error: unknown app '$APP_NAME' (expected: pcbnew | eeschema | calculator | pl_editor)" >&2 + echo "Error: unknown app '$APP_NAME' (expected: pcbnew | eeschema | calculator | pl_editor | symbol_editor)" >&2 exit 1 ;; esac @@ -378,7 +394,7 @@ emcmake cmake "${KICAD_DIR}" \ 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}/${KICAD_TARGET} -I${KICAD_DIR}/common" + KICAD_INCLUDES="-I${KICAD_BUILD} -I${KICAD_DIR}/include -I${KICAD_DIR}/${KICAD_SUBDIR} -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" @@ -410,4 +426,4 @@ 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}/${KICAD_TARGET}/${APP_NAME}.js" +log_info "Output: ${KICAD_BUILD}/${KICAD_SUBDIR}/${APP_NAME}.js" diff --git a/scripts/kicad/build-symbol_editor.sh b/scripts/kicad/build-symbol_editor.sh new file mode 100755 index 0000000..733a45e --- /dev/null +++ b/scripts/kicad/build-symbol_editor.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Build KiCad Symbol 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" symbol_editor "$@" diff --git a/tests/apps/kicad/symbol_editor.html b/tests/apps/kicad/symbol_editor.html new file mode 100644 index 0000000..f18516c --- /dev/null +++ b/tests/apps/kicad/symbol_editor.html @@ -0,0 +1,199 @@ + + + + + + KiCad Symbol Editor WASM + + + +
+ +
+
Initializing...
+
+
+ +
+ + + + + + + + diff --git a/tests/baseline-screenshots/symbol_editor-01-loaded.png b/tests/baseline-screenshots/symbol_editor-01-loaded.png new file mode 100644 index 0000000..b190d43 Binary files /dev/null and b/tests/baseline-screenshots/symbol_editor-01-loaded.png differ diff --git a/tests/baseline-screenshots/symbol_editor-02-metrics.png b/tests/baseline-screenshots/symbol_editor-02-metrics.png new file mode 100644 index 0000000..b190d43 Binary files /dev/null and b/tests/baseline-screenshots/symbol_editor-02-metrics.png differ diff --git a/tests/baseline-screenshots/symbol_editor-wizard-00-initial.png b/tests/baseline-screenshots/symbol_editor-wizard-00-initial.png new file mode 100644 index 0000000..403997e Binary files /dev/null and b/tests/baseline-screenshots/symbol_editor-wizard-00-initial.png differ diff --git a/tests/baseline-screenshots/symbol_editor-wizard-01.png b/tests/baseline-screenshots/symbol_editor-wizard-01.png new file mode 100644 index 0000000..4315300 Binary files /dev/null and b/tests/baseline-screenshots/symbol_editor-wizard-01.png differ diff --git a/tests/baseline-screenshots/symbol_editor-wizard-02.png b/tests/baseline-screenshots/symbol_editor-wizard-02.png new file mode 100644 index 0000000..64ee007 Binary files /dev/null and b/tests/baseline-screenshots/symbol_editor-wizard-02.png differ diff --git a/tests/baseline-screenshots/symbol_editor-wizard-03.png b/tests/baseline-screenshots/symbol_editor-wizard-03.png new file mode 100644 index 0000000..8e44c46 Binary files /dev/null and b/tests/baseline-screenshots/symbol_editor-wizard-03.png differ diff --git a/tests/baseline-screenshots/symbol_editor-wizard-04-finish.png b/tests/baseline-screenshots/symbol_editor-wizard-04-finish.png new file mode 100644 index 0000000..b190d43 Binary files /dev/null and b/tests/baseline-screenshots/symbol_editor-wizard-04-finish.png differ diff --git a/tests/kicad/symbol_editor.spec.ts b/tests/kicad/symbol_editor.spec.ts new file mode 100644 index 0000000..4d764cf --- /dev/null +++ b/tests/kicad/symbol_editor.spec.ts @@ -0,0 +1,105 @@ +import type { Page } from '@playwright/test'; +import { test, expect } from './fixtures'; +import { clickByLabel } from '../e2e/utils/element-tracker'; + +/** + * Symbol Editor WASM E2E Tests + * + * The symbol editor (FRAME_SCH_SYMBOL_EDITOR) is served by the eeschema kiface; + * the standalone `symbol_editor` launcher opens that frame directly. It shares + * the same first-run setup wizard as eeschema/pcbnew, so the launch flow mirrors + * eeschema.spec.ts: wait for the canvas, click through the wizard, then assert the + * editor chrome built. Scope is launch-only — the editor must start, paint a + * canvas + toolbars, populate the element registry, and produce no WASM abort. + * Library load/save and other features are intentionally out of scope here. + */ + +async function completeWizard(page: Page): Promise { + await expect(page.locator('#canvas')).toBeVisible({ timeout: 90000 }); + await page.waitForFunction(() => !!window.wxElementRegistry, null, { timeout: 90000 }); + // The frame builds its UI a beat after the registry object appears; wait for + // the registry to actually have entries (the wizard or the editor itself) + // before driving it, otherwise we screenshot a blank canvas. + await page.waitForFunction(() => { + const registry = window.wxElementRegistry; + return !!registry && registry.findAll({}).length > 0; + }, null, { timeout: 90000 }); + await page.waitForTimeout(2000); + + await page.screenshot({ path: 'test-results/symbol_editor-wizard-00-initial.png', scale: 'device' }); + + for (let i = 1; i <= 10; i++) { + let clicked = await clickByLabel(page, 'Next >'); + + if (!clicked) { + clicked = await clickByLabel(page, 'Finish'); + + if (clicked) { + await page.waitForTimeout(500); + await page.screenshot({ + path: `test-results/symbol_editor-wizard-${String(i).padStart(2, '0')}-finish.png`, + scale: 'device' + }); + } + + break; + } + + await page.waitForTimeout(500); + await page.screenshot({ + path: `test-results/symbol_editor-wizard-${String(i).padStart(2, '0')}.png`, + scale: 'device' + }); + } + + await page.waitForTimeout(2000); +} + +function hasAbort(testLogger: { consoleLogs: string[]; errors: string[] }): boolean { + return [...testLogger.consoleLogs, ...testLogger.errors].some(line => line.includes('Aborted(')); +} + +test.describe('symbol_editor WASM', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/kicad/symbol_editor.html'); + }); + + test('app loads, canvas visible, no WASM abort', async ({ page, testLogger }) => { + await completeWizard(page); + await page.screenshot({ path: 'test-results/symbol_editor-01-loaded.png', scale: 'device' }); + + expect(hasAbort(testLogger), 'no WASM abort during load').toBe(false); + + const canvasCount = await page.locator('canvas').count(); + expect(canvasCount).toBeGreaterThan(0); + }); + + test('canvas + toolbar metrics look sane', async ({ page, testLogger }) => { + await completeWizard(page); + + const metrics = await page.evaluate(() => { + const registry = window.wxElementRegistry; + const all = registry ? registry.findAll({ visible: true }) : []; + const toolbars = all.filter((el: { typeName: string }) => /ToolBar/.test(el.typeName)); + const glCanvas = document.querySelector('canvas[id^="glcanvas-"]') as HTMLCanvasElement | null; + + return { + registryTotal: all.length, + toolbarCount: toolbars.length, + mainCanvasOk: (() => { + const c = document.getElementById('canvas') as HTMLCanvasElement | null; + return !!c && c.width > 0 && c.height > 0; + })(), + glCanvasOk: !!glCanvas && glCanvas.width > 0 && glCanvas.height > 0, + }; + }); + + await page.screenshot({ path: 'test-results/symbol_editor-02-metrics.png', scale: 'device' }); + + expect(metrics.registryTotal, 'registry should be populated').toBeGreaterThan(10); + expect(metrics.toolbarCount, 'at least one toolbar should be visible').toBeGreaterThanOrEqual(1); + expect(metrics.mainCanvasOk, 'main canvas has nonzero dimensions').toBe(true); + expect(metrics.glCanvasOk, 'GL canvas has nonzero dimensions').toBe(true); + expect(hasAbort(testLogger)).toBe(false); + }); +}); diff --git a/tests/package.json b/tests/package.json index 6fd0397..66780fb 100644 --- a/tests/package.json +++ b/tests/package.json @@ -23,6 +23,10 @@ "test:calculator:chrome": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=chromium --headed kicad/calculator.spec.ts", "test:calculator": "npm run test:calculator:firefox", "test:calculator:headed": "npm run test:calculator:chrome", + "test:symbol_editor:firefox": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=firefox kicad/symbol_editor.spec.ts", + "test:symbol_editor:chrome": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=chromium --headed kicad/symbol_editor.spec.ts", + "test:symbol_editor": "npm run test:symbol_editor:firefox", + "test:symbol_editor:headed": "npm run test:symbol_editor: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 ae5cdff..8ed9a9b 100755 --- a/tests/scripts/setup-kicad-wasm.sh +++ b/tests/scripts/setup-kicad-wasm.sh @@ -21,9 +21,10 @@ mkdir -p "$KICAD_TEST" # source lives under pagelayout_editor/. kicad_subdir_for() { case "$1" in - calculator) echo "pcb_calculator" ;; - pl_editor) echo "pagelayout_editor" ;; - *) echo "$1" ;; + calculator) echo "pcb_calculator" ;; + pl_editor) echo "pagelayout_editor" ;; + symbol_editor) echo "eeschema" ;; + *) echo "$1" ;; esac } @@ -63,13 +64,14 @@ copy_app() { } found_any=0 -copy_app pcbnew && found_any=1 -copy_app eeschema && found_any=1 -copy_app calculator && found_any=1 -copy_app pl_editor && found_any=1 +copy_app pcbnew && found_any=1 +copy_app eeschema && found_any=1 +copy_app calculator && found_any=1 +copy_app pl_editor && found_any=1 +copy_app symbol_editor && found_any=1 if [ "$found_any" -eq 0 ]; then - echo "Error: no pcbnew/eeschema/calculator/pl_editor artifacts found in output/ or docker volume" >&2 + echo "Error: no pcbnew/eeschema/calculator/pl_editor/symbol_editor artifacts found in output/ or docker volume" >&2 exit 1 fi