diff --git a/.github/workflows/wasm-build.yml b/.github/workflows/wasm-build.yml index 8c7d00d..042fa33 100644 --- a/.github/workflows/wasm-build.yml +++ b/.github/workflows/wasm-build.yml @@ -27,7 +27,7 @@ on: type: string default: "-O1" build_3d_viewer: - description: "Build the WASM 3D viewer into pcbnew (ON/OFF)" + description: "Build the WASM 3D viewer into kicad_editor (ON/OFF)" type: string default: "ON" run_tests: @@ -190,7 +190,7 @@ jobs: docker run --rm -v kicad-wasm-ci_kicad-build-cache:/bw -v "$PWD/deps-cache":/cache \ alpine sh -c 'tar xzf /cache/deps.tar.gz -C /bw' - # PHASE 1 (base miss only): container compile of all 6 tools → output/ base + # PHASE 1 (base miss only): container compile of all 4 bundles → output/ base # wasm (opt-independent). 3D viewer per input. --compile-only skips the host # asyncify/-O tail. KICAD_PIPELINE has no effect here (no post-process to # overlap), but the deps short-circuit on a warm volume. diff --git a/docker/build.sh b/docker/build.sh index f3d7c31..4a61f35 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -6,12 +6,14 @@ # ./docker/build.sh [,...] [args...] # # Apps: -# pcbnew PCB editor (also serves the footprint editor via --frame=fpedit) -# eeschema schematic editor (also serves the symbol editor via --frame=symedit) +# kicad_editor merged PCB + schematic editor image — serves all four editors +# (PCB / Footprint / Schematic / Symbol) via runtime --frame # calculator PCB calculator # pl_editor drawing-sheet editor # gerbview Gerber viewer # all build all of the above +# pcbnew standalone PCB engine (debug aid; not deployed — kicad_editor is) +# eeschema standalone schematic engine (debug aid; not deployed) # # A comma-separated list builds just those apps in order (e.g. # "calculator,pl_editor" — used to exercise the multi-app pipeline cheaply). @@ -65,7 +67,7 @@ trap 'kw_fail 130; exit 130' INT TERM cd "$(dirname "$0")/.." -VALID_APPS="pcbnew | eeschema | calculator | pl_editor | gerbview | sym_convert | all" +VALID_APPS="kicad_editor | pcbnew | eeschema | calculator | pl_editor | gerbview | sym_convert | all" usage() { echo "Usage: ./docker/build.sh [,...] [args...]" >&2 @@ -88,15 +90,17 @@ APP_NAME="$1" shift # Expand the app argument into APPS[]: "all", a single app, or a comma list. -# pcbnew first in "all" — its 90-min host-side wasm-opt chain is the critical -# path, so it must start as early as possible (especially with KICAD_PIPELINE=1). +# kicad_editor first in "all" — the merged image is the largest bundle, so its +# host-side wasm-opt chain is the critical path and must start as early as +# possible (especially with KICAD_PIPELINE=1). pcbnew/eeschema stay buildable as +# standalone debug aids but are not part of "all" (not deployed). if [[ "$APP_NAME" == "all" ]]; then - APPS=(pcbnew eeschema calculator pl_editor gerbview) + APPS=(kicad_editor calculator pl_editor gerbview) else IFS=',' read -r -a APPS <<< "$APP_NAME" for app in "${APPS[@]}"; do case "$app" in - pcbnew|eeschema|calculator|pl_editor|gerbview|sym_convert) ;; + kicad_editor|pcbnew|eeschema|calculator|pl_editor|gerbview|sym_convert) ;; *) echo "Error: unknown app '$app' (expected: ${VALID_APPS})" >&2 usage diff --git a/kicad b/kicad index 7660513..f5df597 160000 --- a/kicad +++ b/kicad @@ -1 +1 @@ -Subproject commit 766051316ba7de2c4e4f08340dca08100c3e1565 +Subproject commit f5df5977e0977063da6f6b66cf69799f5c90159a diff --git a/scripts/deploy/publish-wasm.mjs b/scripts/deploy/publish-wasm.mjs index 9d82f52..202024f 100644 --- a/scripts/deploy/publish-wasm.mjs +++ b/scripts/deploy/publish-wasm.mjs @@ -29,14 +29,13 @@ import { // --- tools & per-file rules --------------------------------------------------- -// Bundles served to the browser editor. symbol_editor / footprint_editor are NOT -// separate bundles: they are the eeschema / pcbnew bundle booted with a runtime -// --frame flag (editor-unification), so they publish nothing of their own. The -// frontend maps them onto their parent bundle via TOOL_BUNDLE. sym_convert is a -// node CLI, not served. +// Bundles served to the browser editor. The four editor tools (pcbnew, eeschema, +// footprint_editor, symbol_editor) are ALL served by the ONE merged kicad_editor +// bundle, booted with a runtime --frame flag (editor-unification Part 2) — none of +// them publishes anything of its own. The frontend maps tools onto bundles via +// TOOL_BUNDLE. sym_convert is a node CLI, not served. const TOOLS = [ - "pcbnew", - "eeschema", + "kicad_editor", "pl_editor", "gerbview", "calculator", diff --git a/scripts/kicad/audit-merged-symbols.sh b/scripts/kicad/audit-merged-symbols.sh new file mode 100755 index 0000000..2776ee1 --- /dev/null +++ b/scripts/kicad/audit-merged-symbols.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# Duplicate-symbol audit for the merged kicad_editor image (editor-unification Part 2). +# +# The pcbnew and eeschema kifaces are statically linked into one WASM image with +# --allow-multiple-definition (required for wx/KiCad dupes), so a symbol both engines +# define binds SILENTLY to the first definition — a wrong-behavior/memory-corruption +# bug, never a link error. This script intersects the two kifaces' defined symbols so +# every collision is a conscious decision (the known set is renamed per-engine via +# KICAD_WASM_PCB_SIDE_RENAMES in kicad/CMakeLists.txt). +# +# Run INSIDE the build container (needs $EMSDK llvm-nm and the per-app build trees): +# docker compose -f docker/docker-compose.yml exec kicad-wasm-builder \ +# /workspace/scripts/kicad/audit-merged-symbols.sh +# Re-run whenever the kicad submodule is bumped. Expected output: EMPTY intersection +# (all known collisions renamed). Any line of output is a new upstream collision to +# add to the rename list. +# +# Method (see docs/features/editor-unification/06-part2-implementation.md): +# - strong (T/D/B/R) defined external symbols: direct first-def-wins collisions. +# - weak (W/V) symbols are only dangerous when the SAME name has DIFFERENT +# definitions per module (same-named classes with inline members). Weak symbols +# also defined by the shared libs are one shared definition — filtered out. The +# remainder is mostly benign template dedup (magic_enum etc.); review NEW entries. + +set -euo pipefail + +BUILD_ROOT="${BUILD_ROOT:-/workspace/build-wasm}" +P="${BUILD_ROOT}/kicad-pcbnew" +E="${BUILD_ROOT}/kicad-eeschema" +NM="${EMSDK:?EMSDK not set}/upstream/bin/llvm-nm" +OUT="${TMPDIR:-/tmp}/merged-symbol-audit" +mkdir -p "${OUT}" + +if [ ! -d "${P}/pcbnew" ] || [ ! -d "${E}/eeschema" ]; then + echo "ERROR: need built kicad-pcbnew and kicad-eeschema trees under ${BUILD_ROOT}" >&2 + echo " (./docker/build.sh pcbnew,eeschema --compile-only)" >&2 + exit 1 +fi + +pcb_files() { + find "${P}/pcbnew/CMakeFiles/pcbnew_kiface_objects.dir" -name '*.o' + ls "${P}/common/libpcbcommon.a" \ + "${P}/pcbnew/connectivity/libconnectivity.a" \ + "${P}/pcbnew/router/libpnsrouter.a" \ + "${P}/pcbnew/navlib/libpcbnew_navlib.a" \ + "${P}/utils/idftools/libidf3.a" \ + "${P}"/pcbnew/pcb_io/*/*.a 2>/dev/null || true + find "${P}/3d-viewer" -name '*.o' 2>/dev/null || true +} + +sch_files() { + find "${E}/eeschema/CMakeFiles/eeschema_kiface_objects.dir" -name '*.o' + ls "${E}/eeschema/navlib/libeeschema_navlib.a" 2>/dev/null || true +} + +# Weak symbols defined by the SHARED libs (one definition linked once — safe dedup). +shared_syms() { + ${NM} --defined-only --extern-only --format=posix \ + "${P}/common/libcommon.a" "${P}/common/libkicommon.a" \ + "${P}/common/gal/libkigal.a" "${P}/libs/core/libcore.a" \ + "${P}/libs/kimath/libkimath.a" "${P}/libs/kiplatform/libkiplatform.a" \ + "${P}/scripting/libscripting.a" "${P}/api/libkiapi.a" \ + "${P}/libs/sexpr/libsexpr.a" 2>/dev/null | awk '{print $1}' | sort -u +} + +echo "== collecting symbols (llvm-nm)..." >&2 +${NM} --defined-only --extern-only --format=posix $(pcb_files) 2>/dev/null \ + | awk '$2 ~ /^[TDBR]$/ {print $1}' | sort -u > "${OUT}/pcb_strong.txt" +${NM} --defined-only --extern-only --format=posix $(sch_files) 2>/dev/null \ + | awk '$2 ~ /^[TDBR]$/ {print $1}' | sort -u > "${OUT}/sch_strong.txt" +${NM} --defined-only --extern-only --format=posix $(pcb_files) 2>/dev/null \ + | awk '$2 ~ /^[WVwv]$/ {print $1}' | sort -u > "${OUT}/pcb_weak.txt" +${NM} --defined-only --extern-only --format=posix $(sch_files) 2>/dev/null \ + | awk '$2 ~ /^[WVwv]$/ {print $1}' | sort -u > "${OUT}/sch_weak.txt" +shared_syms > "${OUT}/shared.txt" + +# Known-renamed / expected entries. In OPTION-OFF per-app trees (the normal state of +# kicad-pcbnew / kicad-eeschema) the renames are NOT applied, so the known set shows +# up here — filter it; the merged tree compiles with the renames so these cannot +# collide there. KIFACE_1/Kiface get per-engine getter/accessor names; the class/data +# names are prefixed PCB_/pcb (KICAD_WASM_PCB_SIDE_RENAMES). +KNOWN='^(KIFACE_1$|_Z6Kifacev$|_Z16checkOverwriteDb|pcbAllowedActions$|allowedActions$|g_excludedLayers$|_Z[A-Z0-9]*[0-9]+(PCB_)?(DIALOG_TEXT_PROPERTIES|DIALOG_SHAPE_PROPERTIES|DIALOG_TABLE_PROPERTIES|DIALOG_TABLECELL_PROPERTIES|DIALOG_GLOBAL_EDIT_TEXT_AND_GRAPHICS|PANEL_SETUP_FORMATTING|TEXT_SEARCH_HANDLER|GROUP_SEARCH_HANDLER|TEXTBOX_POINT_EDIT_BEHAVIOR|RECTANGLE_POINT_EDIT_BEHAVIOR|FILEDLG_HOOK_SAVE_PROJECT|FOOTPRINT_INFO_GENERATOR))' +# Mangled-name filter needs the raw class tokens too (ZTV/ZTI/ZTS/ZN prefixes). +CLASSES='DIALOG_TEXT_PROPERTIES|DIALOG_SHAPE_PROPERTIES|DIALOG_TABLE_PROPERTIES|DIALOG_TABLECELL_PROPERTIES|DIALOG_GLOBAL_EDIT_TEXT_AND_GRAPHICS|PANEL_SETUP_FORMATTING|TEXT_SEARCH_HANDLER|GROUP_SEARCH_HANDLER|TEXTBOX_POINT_EDIT_BEHAVIOR|RECTANGLE_POINT_EDIT_BEHAVIOR|FILEDLG_HOOK_SAVE_PROJECT|FOOTPRINT_INFO_GENERATOR|checkOverwriteDb|allowedActions|g_excludedLayers' + +echo "== STRONG duplicates (unexpected — must be renamed or justified):" +comm -12 "${OUT}/pcb_strong.txt" "${OUT}/sch_strong.txt" \ + | grep -vE "${CLASSES}" | grep -vE '^KIFACE_1$|^_Z6Kifacev$' || true + +echo "== WEAK duplicates not from shared libs, mentioning NEW class tokens" +echo " (known colliding classes filtered; review anything printed):" +comm -12 "${OUT}/pcb_weak.txt" "${OUT}/sch_weak.txt" \ + | comm -23 - "${OUT}/shared.txt" \ + | grep -vE "${CLASSES}" \ + | grep -E '_Z(TV|TI|TS|N)[0-9]+[A-Z]' \ + | grep -vE 'magic_enum|wxEventFunctorMethod|wxNavigationEnabled|wxSimplebook|wxDataView|wxMenuBar|wxVector|KIFACE|COLLECTOR|RC_JSON|PARAM_SCALED|EDA_|BOX2|WX_MENUBAR|SEARCH_HANDLER' || true + +echo "== audit done (details in ${OUT})" diff --git a/scripts/kicad/build-kicad-target.sh b/scripts/kicad/build-kicad-target.sh index 24ca569..e434521 100755 --- a/scripts/kicad/build-kicad-target.sh +++ b/scripts/kicad/build-kicad-target.sh @@ -31,7 +31,7 @@ set -e if [ -z "$1" ]; then - echo "Error: missing argument (pcbnew | eeschema | calculator | pl_editor | gerbview)" >&2 + echo "Error: missing argument (kicad_editor | pcbnew | eeschema | calculator | pl_editor | gerbview)" >&2 exit 1 fi APP_NAME="$1" @@ -45,6 +45,15 @@ shift # The footprint/symbol editors have no target of their own — the pcbnew/eeschema # bundle opens them at runtime via single_top.cpp's --frame flag. case "$APP_NAME" in + kicad_editor) + # Merged pcbnew+eeschema image (editor-unification Part 2): one executable + # linking BOTH kifaces, frame chosen at runtime by --frame (pcb/fpedit/sch/ + # symedit). Target lives in wasm/editor/ (added by the fork's top-level + # CMakeLists under -DKICAD_WASM_MERGED_EDITOR=ON); its binary dir doubles as + # the artifact subdir. + KICAD_TARGET="kicad_editor" + KICAD_SUBDIR="kicad_editor" + ;; pcbnew|eeschema|gerbview) KICAD_TARGET="$APP_NAME" KICAD_SUBDIR="$APP_NAME" @@ -65,7 +74,7 @@ case "$APP_NAME" in KICAD_SUBDIR="eeschema" ;; *) - echo "Error: unknown app '$APP_NAME' (expected: pcbnew | eeschema | calculator | pl_editor | gerbview | sym_convert)" >&2 + echo "Error: unknown app '$APP_NAME' (expected: kicad_editor | pcbnew | eeschema | calculator | pl_editor | gerbview | sym_convert)" >&2 exit 1 ;; esac @@ -80,9 +89,12 @@ esac # Which app's WASM stub libraries (scripting/frame placeholders) to link. # sym_convert links the eeschema kiface objects, so it needs eeschema's frame -# stub (eeschema_frame_stub.cpp). +# stub (eeschema_frame_stub.cpp). kicad_editor links pcbnew's kiface objects, which +# reference the action-plugin scripting placeholders (pcbnewGet*); eeschema's frame +# stub arrives via CMake (target_sources on eeschema_kiface_objects), not this path. case "$APP_NAME" in sym_convert) STUB_APP="eeschema" ;; + kicad_editor) STUB_APP="pcbnew" ;; *) STUB_APP="$APP_NAME" ;; esac @@ -370,7 +382,15 @@ log_info "KiCad WASM support verified" # 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}/${EMBIND_APP}_embind.o" +# kicad_editor (merged image) links THREE embind objects: both per-editor TUs +# (compiled with -DKICAD_MERGED_EMBIND, which compiles out their duplicate +# definitions and shared-name registrations) plus the dispatcher TU that registers +# the shared JS names once — see wasm/bindings/kicad_editor_embind.cpp. +if [ "${APP_NAME}" = "kicad_editor" ]; then + EMBIND_OBJ="${STUBS_BUILD}/pcbnew_embind.o ${STUBS_BUILD}/eeschema_embind.o ${STUBS_BUILD}/kicad_editor_embind.o" +else + EMBIND_OBJ="${STUBS_BUILD}/${EMBIND_APP}_embind.o" +fi EMBIND_SRC="${PROJECT_ROOT}/wasm/bindings/${EMBIND_APP}_embind.cpp" # Step 7: Configure KiCad with CMake @@ -392,6 +412,15 @@ if [ "${APP_NAME}" = "sym_convert" ]; then SYM_CONVERTER_CMAKE_FLAG="-DKICAD_SYM_CONVERTER_WASM=ON" fi +# Merged pcbnew+eeschema editor: gates the wasm/editor/ subdir, the per-engine +# Kiface()/KIFACE_GETTER renames, and the PCB-side ODR symbol renames in the fork's +# CMake (see KICAD_WASM_MERGED_EDITOR in kicad/CMakeLists.txt). Only this tree +# (build-wasm/kicad-kicad_editor) configures with it ON. +MERGED_EDITOR_CMAKE_FLAG="" +if [ "${APP_NAME}" = "kicad_editor" ]; then + MERGED_EDITOR_CMAKE_FLAG="-DKICAD_WASM_MERGED_EDITOR=ON" +fi + # 3D viewer: built by DEFAULT (BUILD_3D_VIEWER=ON). Opt out with BUILD_3D_VIEWER=OFF, which links the # 3D stubs instead. The 3D viewer renders with the GL-free CPU raytracer (RENDER_3D_RAYTRACE_RAM) # blitted to the canvas through a plain WebGL2 textured quad — no -sLEGACY_GL_EMULATION. KiCad's @@ -442,6 +471,7 @@ fi emcmake cmake "${KICAD_DIR}" \ ${CCACHE_OPTS} \ ${SYM_CONVERTER_CMAKE_FLAG} \ + ${MERGED_EDITOR_CMAKE_FLAG} \ -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ -DCMAKE_INSTALL_PREFIX="${SYSROOT}" \ -DCMAKE_MODULE_PATH="${WASM_LAYER}/cmake" \ @@ -495,7 +525,51 @@ emcmake cmake "${KICAD_DIR}" \ # 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 + +# Compile one embind TU with the same includes and ABI-critical flags KiCad uses +# (KICAD_TU_ABI_FLAGS must match the core's -DDEBUG state or vtable dispatch skews — +# task #54). Args: [extra-defines] +compile_embind_tu() { + local _src="$1" _obj="$2" _subdir="$3" _defines="${4:-}" + + local _includes="-I${KICAD_BUILD} -I${KICAD_DIR}/include -I${KICAD_DIR}/${_subdir} -I${KICAD_DIR}/common" + # Generated DSN-lexer headers (e.g. pcb_lexer.h, used transitively via kicad_clipboard.h → + # pcb_io_kicad_sexpr_parser.h) are emitted into the common build subdir by make_lexer. + _includes+=" -I${KICAD_BUILD}/common" + _includes+=" -I${KICAD_DIR}/libs/core/include -I${KICAD_DIR}/libs/kimath/include -I${KICAD_DIR}/libs/kiplatform/include" + _includes+=" -I${KICAD_DIR}/thirdparty/clipper2/Clipper2Lib/include" + _includes+=" -I${KICAD_DIR}/thirdparty/nlohmann_json" + _includes+=" -I${KICAD_DIR}/thirdparty/expected/include" + _includes+=" -I${KICAD_DIR}/thirdparty/rtree" + _includes+=" -I${KICAD_DIR}/thirdparty/fmt" + _includes+=" -I${KICAD_DIR}/thirdparty/dynamic_bitset" + _includes+=" -I${KICAD_DIR}/thirdparty/nanodbc" + _includes+=" -I${KICAD_DIR}/thirdparty/picosha2" + _includes+=" -I${KICAD_DIR}/thirdparty" + # libcontext.h lives one level deeper; tool/coroutine.h does #include + _includes+=" -I${KICAD_DIR}/thirdparty/libcontext" + _includes+=" -I${SYSROOT}/include" + + # KiCad requires C++20 for concepts + em++ -std=c++20 -c ${EXTRA_FLAGS} ${KICAD_TU_ABI_FLAGS} ${WX_CXXFLAGS} ${_defines} ${_includes} "${_src}" -o "${_obj}" +} + +if [ "${APP_NAME}" = "kicad_editor" ]; then + # Merged image: both per-editor TUs (with -DKICAD_MERGED_EMBIND compiling out + # their duplicate definitions / shared-name registrations) + the dispatcher TU + # registering the shared JS names once (wasm/bindings/kicad_editor_embind.cpp). + # pcbnew's TU needs the generated lexer headers — pre-build pcbcommon (no wasted + # work: kicad_editor depends on pcbcommon anyway; incremental no-op). + log_info "Pre-building pcbcommon so generated lexer headers exist for the embind compile..." + emmake make -j${JOBS} pcbcommon + log_info "Compiling merged Embind bindings (pcbnew + eeschema + dispatcher)..." + compile_embind_tu "${PROJECT_ROOT}/wasm/bindings/pcbnew_embind.cpp" \ + "${STUBS_BUILD}/pcbnew_embind.o" pcbnew "-DKICAD_MERGED_EMBIND" + compile_embind_tu "${PROJECT_ROOT}/wasm/bindings/eeschema_embind.cpp" \ + "${STUBS_BUILD}/eeschema_embind.o" eeschema "-DKICAD_MERGED_EMBIND" + compile_embind_tu "${PROJECT_ROOT}/wasm/bindings/kicad_editor_embind.cpp" \ + "${STUBS_BUILD}/kicad_editor_embind.o" common +elif [ -f "${EMBIND_SRC}" ]; then # pcbnew's embind TU transitively includes generated lexer headers # (kicad_clipboard.h → pcb_io_kicad_sexpr_parser.h → pcb_lexer.h, emitted into # ${KICAD_BUILD}/common by make_lexer custom commands on the pcbcommon target). @@ -508,26 +582,7 @@ if [ -f "${EMBIND_SRC}" ]; then emmake make -j${JOBS} pcbcommon fi 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_SUBDIR} -I${KICAD_DIR}/common" - # Generated DSN-lexer headers (e.g. pcb_lexer.h, used transitively via kicad_clipboard.h → - # pcb_io_kicad_sexpr_parser.h) are emitted into the common build subdir by make_lexer. - KICAD_INCLUDES+=" -I${KICAD_BUILD}/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/expected/include" - KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/rtree" - KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/fmt" - 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" - # libcontext.h lives one level deeper; tool/coroutine.h does #include - KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/libcontext" - KICAD_INCLUDES+=" -I${SYSROOT}/include" - # KiCad requires C++20 for concepts - em++ -std=c++20 -c ${EXTRA_FLAGS} ${KICAD_TU_ABI_FLAGS} ${WX_CXXFLAGS} ${KICAD_INCLUDES} "${EMBIND_SRC}" -o "${EMBIND_OBJ}" + compile_embind_tu "${EMBIND_SRC}" "${EMBIND_OBJ}" "${KICAD_SUBDIR}" 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" @@ -545,10 +600,14 @@ log_info "Building ${APP_NAME} (CMake target: ${KICAD_TARGET})..." # whenever the freshly-compiled embind object is newer than the linked output. LINK_OUT_JS="${KICAD_BUILD}/${KICAD_SUBDIR}/${APP_NAME}.js" LINK_OUT_WASM="${KICAD_BUILD}/${KICAD_SUBDIR}/${APP_NAME}.wasm" -if [ -f "${EMBIND_OBJ}" ] && [ -f "${LINK_OUT_JS}" ] && [ "${EMBIND_OBJ}" -nt "${LINK_OUT_JS}" ]; then - log_info "Embind object newer than ${APP_NAME}.js — forcing relink to pick up new bindings" - rm -f "${LINK_OUT_JS}" "${LINK_OUT_WASM}" -fi +# EMBIND_OBJ may hold several objects (kicad_editor) — check each. +for _embind_obj in ${EMBIND_OBJ}; do + if [ -f "${_embind_obj}" ] && [ -f "${LINK_OUT_JS}" ] && [ "${_embind_obj}" -nt "${LINK_OUT_JS}" ]; then + log_info "Embind object newer than ${APP_NAME}.js — forcing relink to pick up new bindings" + rm -f "${LINK_OUT_JS}" "${LINK_OUT_WASM}" + break + fi +done emmake make -j${JOBS} "${KICAD_TARGET}" diff --git a/scripts/kicad/build-kicad_editor.sh b/scripts/kicad/build-kicad_editor.sh new file mode 100755 index 0000000..c2701f5 --- /dev/null +++ b/scripts/kicad/build-kicad_editor.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Build the merged KiCad editor (pcbnew + eeschema kifaces in ONE image) for +# WebAssembly. The editor frame (PCB / Footprint / Schematic / Symbol) is chosen +# at runtime via single_top.cpp's --frame flag (editor-unification Part 2). +# 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" kicad_editor "$@" diff --git a/tests/apps/kicad/eeschema.html b/tests/apps/kicad/eeschema.html index 63dd005..3374022 100644 --- a/tests/apps/kicad/eeschema.html +++ b/tests/apps/kicad/eeschema.html @@ -154,6 +154,11 @@ var Module = { thisProgram: '/usr/bin/eeschema', // Fake absolute path for argv[0] (KiCad DEBUG check) + // The schematic editor is served by the merged kicad_editor bundle (editor- + // unification Part 2): load kicad_editor.js below and select FRAME_SCH at + // runtime. + arguments: ['--frame=sch'], + preRun: [createCanvas, writeResources, seedKicadConfig], postRun: [], @@ -209,6 +214,7 @@ - + + diff --git a/tests/apps/kicad/footprint_editor.html b/tests/apps/kicad/footprint_editor.html index a0e173f..4639898 100644 --- a/tests/apps/kicad/footprint_editor.html +++ b/tests/apps/kicad/footprint_editor.html @@ -108,8 +108,9 @@ var Module = { thisProgram: '/usr/bin/footprint_editor', // argv[0] for KiCad's DEBUG check - // The footprint editor is served by the pcbnew bundle (editor-unification): - // load pcbnew.js below and select FRAME_FOOTPRINT_EDITOR at runtime. + // The footprint editor is served by the merged kicad_editor bundle (editor- + // unification Part 2): load kicad_editor.js below and select + // FRAME_FOOTPRINT_EDITOR at runtime. arguments: ['--frame=fpedit'], preRun: [createCanvas, writeResources, seedKicadConfig], @@ -147,7 +148,7 @@ - - + + diff --git a/tests/apps/kicad/pcbnew-collab.html b/tests/apps/kicad/pcbnew-collab.html index 8bec39c..74d4311 100644 --- a/tests/apps/kicad/pcbnew-collab.html +++ b/tests/apps/kicad/pcbnew-collab.html @@ -158,6 +158,11 @@ var Module = { thisProgram: '/usr/bin/pcbnew', // Fake absolute path for argv[0] (KiCad DEBUG check) + // The PCB editor is served by the merged kicad_editor bundle (editor- + // unification Part 2): load kicad_editor.js below and select + // FRAME_PCB_EDITOR at runtime. + arguments: ['--frame=pcb'], + preRun: [createCanvas, writeResources, seedKicadConfig], postRun: [], @@ -213,6 +218,7 @@ - + + diff --git a/tests/apps/kicad/pcbnew.html b/tests/apps/kicad/pcbnew.html index 01d5e2b..d7edbc1 100644 --- a/tests/apps/kicad/pcbnew.html +++ b/tests/apps/kicad/pcbnew.html @@ -123,6 +123,11 @@ var Module = { thisProgram: '/usr/bin/pcbnew', // Fake absolute path for argv[0] (KiCad DEBUG check) + // The PCB editor is served by the merged kicad_editor bundle (editor- + // unification Part 2): load kicad_editor.js below and select + // FRAME_PCB_EDITOR at runtime. + arguments: ['--frame=pcb'], + preRun: [createCanvas, writeResources], postRun: [], @@ -178,6 +183,7 @@ - + + diff --git a/tests/apps/kicad/symbol_editor.html b/tests/apps/kicad/symbol_editor.html index 7bc25e0..63af5f3 100644 --- a/tests/apps/kicad/symbol_editor.html +++ b/tests/apps/kicad/symbol_editor.html @@ -147,8 +147,9 @@ var Module = { thisProgram: '/usr/bin/symbol_editor', // Fake absolute path for argv[0] (KiCad DEBUG check) - // The symbol editor is served by the eeschema bundle (editor-unification): - // load eeschema.js below and select FRAME_SCH_SYMBOL_EDITOR at runtime. + // The symbol editor is served by the merged kicad_editor bundle (editor- + // unification Part 2): load kicad_editor.js below and select + // FRAME_SCH_SYMBOL_EDITOR at runtime. arguments: ['--frame=symedit'], preRun: [createCanvas, writeResources, seedKicadConfig], @@ -206,7 +207,7 @@ - - + + diff --git a/tests/kicad/frame-runtime.spec.ts b/tests/kicad/frame-runtime.spec.ts index c71e55b..3c0d68c 100644 --- a/tests/kicad/frame-runtime.spec.ts +++ b/tests/kicad/frame-runtime.spec.ts @@ -3,34 +3,47 @@ import { test, expect } from '@playwright/test'; /** * Editor-unification runtime-frame validation. * - * After unification the library editors are no longer separate .wasm bundles: - * - the Symbol Editor is the eeschema bundle booted with --frame=symedit - * - the Footprint Editor is the pcbnew bundle booted with --frame=fpedit - * The frame token is threaded through Module.arguments and parsed in - * kicad/common/single_top.cpp (mirroring kicad/kicad.cpp's --frame parser). + * Since Part 2 ALL FOUR editors are served by the ONE merged kicad_editor bundle + * (pcbnew + eeschema kifaces statically linked): each harness loads kicad_editor.js + * with its frame token in Module.arguments — + * - PCB Editor --frame=pcb (also the bundle's build-time default) + * - Footprint Editor --frame=fpedit + * - Schematic Editor --frame=sch + * - Symbol Editor --frame=symedit + * parsed in kicad/common/single_top.cpp (mirroring kicad/kicad.cpp's --frame parser). * - * This asserts the decisive fact the launch-smoke specs don't: that the shared - * bundle actually opens the LIBRARY editor frame, not its parent editor. The - * window title is the discriminator — the parent bundle's default frame would - * title itself "Schematic Editor" / "PCB Editor". + * This asserts the decisive fact the launch-smoke specs don't: that the merged + * bundle actually opens the REQUESTED frame. The window title is the discriminator — + * a dropped/ignored token would land on the build default ("PCB Editor") or a + * sibling editor's title. */ interface FrameCase { harness: string; - /** title the library-editor frame settles on */ + /** title the requested editor frame settles on */ titleRe: RegExp; - /** the parent editor's title — must NOT appear (would mean --frame was ignored) */ - parentRe: RegExp; + /** any OTHER editor's title — must NOT appear (would mean --frame went wrong) */ + wrongRe: RegExp; } const CASES: FrameCase[] = [ - { harness: 'symbol_editor.html', titleRe: /Symbol Editor/i, parentRe: /Schematic Editor/i }, - { harness: 'footprint_editor.html', titleRe: /Footprint Editor/i, parentRe: /PCB Editor/i }, + { harness: 'pcbnew.html', + titleRe: /PCB Editor/i, + wrongRe: /Schematic Editor|Symbol Editor|Footprint Editor/i }, + { harness: 'footprint_editor.html', + titleRe: /Footprint Editor/i, + wrongRe: /PCB Editor|Schematic Editor|Symbol Editor/i }, + { harness: 'eeschema.html', + titleRe: /Schematic Editor/i, + wrongRe: /PCB Editor|Footprint Editor|Symbol Editor/i }, + { harness: 'symbol_editor.html', + titleRe: /Symbol Editor/i, + wrongRe: /PCB Editor|Schematic Editor|Footprint Editor/i }, ]; test.describe('editor-unification runtime frame (--frame)', () => { for (const tc of CASES) { - test(`${tc.harness} opens the library editor frame from its parent bundle`, async ({ page }) => { + test(`${tc.harness} opens its editor frame from the merged bundle`, async ({ page }) => { const consoleLines: string[] = []; page.on('console', (m) => consoleLines.push(m.text())); page.on('pageerror', (e) => consoleLines.push(`pageerror: ${e.message}`)); @@ -43,7 +56,7 @@ test.describe('editor-unification runtime frame (--frame)', () => { // The frame sets the document title once it is up; poll until it settles. await expect .poll(() => page.title(), { - message: `${tc.harness}: never reached the expected library-editor title`, + message: `${tc.harness}: never reached the expected editor title`, timeout: 120000, intervals: [1000], }) @@ -53,13 +66,14 @@ test.describe('editor-unification runtime frame (--frame)', () => { // eslint-disable-next-line no-console console.log(`[frame-runtime] ${tc.harness} -> title=${JSON.stringify(title)}`); - // The runtime --frame flag actually switched frames: the parent editor's - // title must not be what we ended up on. - expect(title, `${tc.harness}: opened the library editor, not its parent`).not.toMatch(tc.parentRe); + // The runtime --frame flag actually selected the right frame: no sibling + // editor's title. + expect(title, `${tc.harness}: opened the requested editor, not a sibling`).not.toMatch(tc.wrongRe); - // No WASM abort during load. - const aborted = consoleLines.some((l) => /Aborted\(/.test(l)); - expect(aborted, 'no WASM abort during load').toBe(false); + // No WASM abort during load — and no duplicate embind registration (the + // merged dispatcher must register each shared JS name exactly once). + const aborted = consoleLines.some((l) => /Aborted\(|Cannot register public name/.test(l)); + expect(aborted, 'no WASM abort / duplicate embind registration during load').toBe(false); await page.screenshot({ path: `test-results/frame-runtime-${tc.harness}.png`, scale: 'device' }); }); diff --git a/tests/playwright-kicad.config.ts b/tests/playwright-kicad.config.ts index 54af3b8..7b0d222 100644 --- a/tests/playwright-kicad.config.ts +++ b/tests/playwright-kicad.config.ts @@ -62,12 +62,15 @@ function findFreePort(): number { const port = resolvePort(); -// pcbnew's wasm (~190M debug build) exceeds SpiderMonkey's per-process code +// The merged kicad_editor wasm (~190M+ debug build; pcbnew+eeschema engines in one +// image since editor-unification Part 2) exceeds SpiderMonkey's per-process code // budget on x86-64 CI even with the baseline-only JIT (run 27343416511: // "InternalError: out of memory" at instantiation; the same module compiles -// on arm64, whose denser code fits). V8 handles it, so on CI these specs run -// on bundled Chromium (the 'chromium-ci' project) and firefox skips them. -const PCBNEW_FAMILY_SPECS = [ +// on arm64, whose denser code fits). V8 handles it, so on CI every spec that +// boots the merged module — i.e. ALL FOUR editors — runs on bundled Chromium +// (the 'chromium-ci' project) and firefox skips them. Firefox keeps the small +// separate bundles (pl_editor / calculator / gerbview). +const BIG_MODULE_SPECS = [ "**/pcbnew.spec.ts", "**/pcbnew-collab.spec.ts", "**/load-pcb.spec.ts", @@ -75,9 +78,9 @@ const PCBNEW_FAMILY_SPECS = [ // Specs added 2026-06-11..06-13 that boot pcbnew (pcbnew.html / // pcbnew-collab.html). Without routing here they ran on Firefox in CI and // timed out at instantiation (the SpiderMonkey/x86 code-budget OOM above). - // The last three are parametrized across pl_editor/eeschema/pcbnew; routing - // the whole file moves those variants to chromium-ci too (they boot fine on - // V8) — only the browser exercising them changes, not whether they run. + // appearance/roundtrip/save-hook are parametrized across pl_editor/eeschema/ + // pcbnew; routing the whole file moves the small-bundle variants to chromium-ci + // too (they boot fine on V8) — only the browser changes, not whether they run. "**/appearance.spec.ts", "**/contextmenu-scrollbar-pcbnew.spec.ts", "**/dark-mode.spec.ts", @@ -94,9 +97,18 @@ const PCBNEW_FAMILY_SPECS = [ "**/3d-viewer-deadlock.spec.ts", "**/3d-viewer-models.spec.ts", "**/footprint-3d-preview.spec.ts", - // Parametrized over both library editors; the footprint case boots the pcbnew - // module (footprint_editor.html loads pcbnew.js) — same V8 routing, whole file. + // Parametrized over the library editors — both cases now boot the merged module. "**/frame-runtime.spec.ts", + // eeschema family: since Part 2 these boot the SAME merged kicad_editor module + // (eeschema.html / symbol_editor.html load kicad_editor.js) — same V8 routing. + "**/eeschema.spec.ts", + "**/eeschema-collab.spec.ts", + "**/eeschema-crosshair.spec.ts", + "**/eeschema-load.spec.ts", + "**/eeschema-subschema.spec.ts", + "**/eeschema-ui.spec.ts", + "**/eeschema-url-regex.spec.ts", + "**/symbol_editor.spec.ts", ]; // Runtime-perf specs run ONLY on the Chromium 'perf' project below: they need @@ -136,10 +148,11 @@ export default defineConfig({ // Firefox is the default for headless testing (works on ARM Mac) name: "firefox", // Perf specs always run on the dedicated 'perf' project, never here. On CI - // the pcbnew-family specs also move to chromium-ci (see PCBNEW_FAMILY_SPECS). + // every merged-module (kicad_editor) spec moves to chromium-ci (see + // BIG_MODULE_SPECS). testIgnore: [ ...PERF_SPECS, - ...(process.env.CI ? PCBNEW_FAMILY_SPECS : []), + ...(process.env.CI ? BIG_MODULE_SPECS : []), ], use: { ...devices["Desktop Firefox"], @@ -159,7 +172,7 @@ export default defineConfig({ // Skip the no-GPU blocklist ("AllowWebgl2:false restricts // context creation") so the GAL canvas gets a WebGL context. "webgl.force-enabled": true, - // pcbnew.wasm (~190M) OOMs the optimizing wasm JIT at compile + // kicad_editor.wasm (~190M+) OOMs the optimizing wasm JIT at compile // time ("InternalError: out of memory") and the app never boots. // Baseline-only compilation trades runtime speed for a compile // that fits in memory. @@ -184,13 +197,13 @@ export default defineConfig({ }, }, { - // CI-only carrier for the pcbnew-family specs (see PCBNEW_FAMILY_SPECS): + // CI-only carrier for the merged-module specs (see BIG_MODULE_SPECS): // Playwright-bundled Chromium, headless, software WebGL via SwiftShader // (fine on x86 Linux; the SwiftShader bug above is ARM-Mac-specific). // --enable-unsafe-swiftshader: newer Chromium refuses software WebGL in // headless without it. name: "chromium-ci", - testMatch: PCBNEW_FAMILY_SPECS, + testMatch: BIG_MODULE_SPECS, use: { ...devices["Desktop Chrome"], viewport: { width: 1280, height: 720 }, diff --git a/tests/scripts/setup-kicad-wasm.sh b/tests/scripts/setup-kicad-wasm.sh index 8948b9b..efe91db 100755 --- a/tests/scripts/setup-kicad-wasm.sh +++ b/tests/scripts/setup-kicad-wasm.sh @@ -79,17 +79,17 @@ copy_app() { } found_any=0 -# symbol_editor / footprint_editor are NOT separate bundles: their harness HTMLs -# (symbol_editor.html / footprint_editor.html) load the eeschema / pcbnew bundle -# and select the frame at runtime via --frame (editor-unification). -copy_app pcbnew && found_any=1 -copy_app eeschema && found_any=1 +# The four editors (pcbnew / eeschema / footprint_editor / symbol_editor) are ALL +# served by the ONE merged kicad_editor bundle: their harness HTMLs load +# kicad_editor.js and select the frame at runtime via --frame (editor-unification +# Part 2). +copy_app kicad_editor && found_any=1 copy_app calculator && found_any=1 copy_app pl_editor && found_any=1 copy_app gerbview && found_any=1 if [ "$found_any" -eq 0 ]; then - echo "Error: no pcbnew/eeschema/calculator/pl_editor/gerbview artifacts found in output/ or docker volume" >&2 + echo "Error: no kicad_editor/calculator/pl_editor/gerbview artifacts found in output/ or docker volume" >&2 exit 1 fi diff --git a/wasm/bindings/eeschema_embind.cpp b/wasm/bindings/eeschema_embind.cpp index 0332368..214f0f5 100644 --- a/wasm/bindings/eeschema_embind.cpp +++ b/wasm/bindings/eeschema_embind.cpp @@ -54,6 +54,13 @@ using json = nlohmann::json; // frame is the app's top window and is a KIWAY_PLAYER. Returns the result of // OpenProjectFiles, or false if no frame is available — letting the JS caller // fall back to driving File→Open. +// +// KICAD_MERGED_EMBIND (kicad_editor, editor-unification Part 2): pcbnew_embind.cpp +// defines the identical function and registers the same JS names — in the merged image +// the frame-agnostic duplicates (this + kicadCollabOnSave) and the shared-name +// registrations live once in kicad_editor_embind.cpp, which dispatches the per-editor +// entries (renamed schCollab*/pcbCollab* below; JS-facing names are unchanged). +#ifndef KICAD_MERGED_EMBIND bool kicadOpenFile( std::string path ) { KIWAY_PLAYER* frame = @@ -68,6 +75,7 @@ bool kicadOpenFile( std::string path ) return frame->OpenProjectFiles( std::vector( 1, wxString::FromUTF8( path.c_str() ) ) ); } +#endif // !KICAD_MERGED_EMBIND // ───────────────────────────── Yjs collaborative bridge ───────────────────────────── // @@ -863,7 +871,7 @@ void collabTestMove( SCH_EDIT_FRAME* aFrame, SCH_ITEM* aItem, SCH_SCREEN* aScree // wxEvtHandler::CallAfter queues onto the app's pending-event list, which the wasm main // loop drains every frame via ProcessPendingEvents() (src/wasm/evtloop.cpp) — i.e. the // exact context real UI edits run in. So defer the whole mutation there. -void kicadCollabApply( std::string aJson ) +void schCollabApply( std::string aJson ) { json delta = json::parse( aJson, nullptr, /*allow_exceptions*/ false ); @@ -896,7 +904,7 @@ void kicadCollabApply( std::string aJson ) // JS pull of the full current model as an all-"added" delta (seed/baseline). Also // registers the change listener on first call. -std::string kicadCollabSnapshot() +std::string schCollabSnapshot() { ensureBridge(); json added = snapshotItems( schFrame() ); @@ -912,7 +920,7 @@ std::string kicadCollabSnapshot() // JS → C++, v2 items wire. Same CallAfter + COROUTINE context as kicadCollabApply // (LoadContent + SCH_COMMIT must run where native edits run). -void kicadCollabApplyItems( std::string aJson ) +void schCollabApplyItems( std::string aJson ) { json wire = json::parse( aJson, nullptr, /*allow_exceptions*/ false ); @@ -938,7 +946,7 @@ void kicadCollabApplyItems( std::string aJson ) // JS pull of the ACTIVE screen's model as an all-"added" v2 items wire: one clipboard- // style blob per item on the current sheet (one collab room == one .kicad_sch screen). // Registers the listener + rebaselines exactly like kicadCollabSnapshot. -std::string kicadCollabSnapshotItems() +std::string schCollabSnapshotItems() { SCH_EDIT_FRAME* fr = schFrame(); @@ -965,7 +973,7 @@ std::string kicadCollabSnapshotItems() // Test/PoC helper: move the first schematic item by (dx,dy) IU via a real SCH_COMMIT, // firing the listener — a deterministic local edit for the two-tab demo / e2e. // Returns the moved item's uuid. -std::string kicadCollabTestMoveFirst( int aDx, int aDy ) +std::string schCollabTestMoveFirst( int aDx, int aDy ) { SCH_EDIT_FRAME* fr = schFrame(); @@ -993,7 +1001,7 @@ std::string kicadCollabTestMoveFirst( int aDx, int aDy ) // Test helper: read an item's position by uuid as "x,y" (internal units). -std::string kicadCollabGetPos( std::string aId ) +std::string schCollabGetPos( std::string aId ) { SCH_EDIT_FRAME* fr = schFrame(); @@ -1022,6 +1030,9 @@ std::string kicadCollabGetPos( std::string aId ) // kicad fork's save chokepoint (SCH_EDIT_FRAME::saveSchematicFile) after a // successful write to MEMFS, so the web app can route the saved bytes onward // (API upload, local-disk write-back, download). No-op without a JS listener. +// KICAD_MERGED_EMBIND: identical definition in pcbnew_embind.cpp; the merged image +// gets the one in kicad_editor_embind.cpp (both fork save chokepoints call it). +#ifndef KICAD_MERGED_EMBIND extern "C" void kicadCollabOnSave( const char* aPath ) { EM_ASM( { @@ -1029,6 +1040,14 @@ extern "C" void kicadCollabOnSave( const char* aPath ) window.kicadCollab.onSave( UTF8ToString( $0 ) ); }, aPath ); } +#endif // !KICAD_MERGED_EMBIND + +// Merged-image dispatch probe (kicad_editor_embind.cpp): is the active top window the +// schematic editor? Counterpart of pcbnew_embind.cpp's pcbEditorActive(). +bool schEditorActive() +{ + return schFrame() != nullptr; +} void kicadSaveSchematic( std::string path ) @@ -1064,17 +1083,22 @@ void kicadSaveSchematic( std::string path ) EMSCRIPTEN_BINDINGS(eeschema) { - // Programmatic file open (preferred over UI automation from the web app). - function("kicadOpenFile", &kicadOpenFile); // Programmatic save of the in-memory schematic (round-trip tests, README §A). function("kicadSaveSchematic", &kicadSaveSchematic); + +#ifndef KICAD_MERGED_EMBIND + // JS names ALSO registered by pcbnew_embind.cpp — in the merged image these are + // registered once by kicad_editor_embind.cpp, dispatching on the active frame. + // Programmatic file open (preferred over UI automation from the web app). + function("kicadOpenFile", &kicadOpenFile); // Yjs collaborative bridge entry points (same contract as pl_editor). - function("kicadCollabApply", &kicadCollabApply); - function("kicadCollabSnapshot", &kicadCollabSnapshot); + function("kicadCollabApply", &schCollabApply); + function("kicadCollabSnapshot", &schCollabSnapshot); // v2 items bridge: per-item s-expr payloads (ysync 0008). - function("kicadCollabApplyItems", &kicadCollabApplyItems); - function("kicadCollabSnapshotItems", &kicadCollabSnapshotItems); - function("kicadCollabTestMoveFirst", &kicadCollabTestMoveFirst); - function("kicadCollabGetPos", &kicadCollabGetPos); + function("kicadCollabApplyItems", &schCollabApplyItems); + function("kicadCollabSnapshotItems", &schCollabSnapshotItems); + function("kicadCollabTestMoveFirst", &schCollabTestMoveFirst); + function("kicadCollabGetPos", &schCollabGetPos); +#endif // !KICAD_MERGED_EMBIND } #endif diff --git a/wasm/bindings/kicad_editor_embind.cpp b/wasm/bindings/kicad_editor_embind.cpp new file mode 100644 index 0000000..6e29fbe --- /dev/null +++ b/wasm/bindings/kicad_editor_embind.cpp @@ -0,0 +1,136 @@ +/* + * Embind dispatcher for the merged kicad_editor WASM image (editor-unification Part 2). + * + * The pcbnew and eeschema binding TUs each implement the collab bridge for their own + * frame and, standalone, register the SAME JS-facing names. In the merged image both + * TUs are compiled with -DKICAD_MERGED_EMBIND, which + * - compiles out their duplicate frame-agnostic definitions (kicadOpenFile, + * extern "C" kicadCollabOnSave) — the single definitions live HERE, and + * - compiles out their shared-name EMSCRIPTEN_BINDINGS registrations — registered + * once HERE, dispatching to the renamed per-editor entries (pcbCollab… and + * schCollab…) on whichever editor frame is live. Per-editor unique names + * (kicadSaveBoard, kicadSaveSchematic, kicadCollabTestItemBlob, Board_…) keep + * flowing from the per-editor blocks unchanged. + * + * With the one-frame-per-page-load model exactly one of pcbEditorActive() / + * schEditorActive() is true — the same dynamic_cast probe every per-editor entry + * already starts with. JS-facing names and signatures are IDENTICAL to the standalone + * bundles, so the web app and tests need no per-bundle API differences. + * + * Deliberately header-light: no pcbnew/eeschema headers (avoids mixing both include + * roots in one TU); only the generic KIWAY_PLAYER surface + the extern declarations. + */ + +#ifdef __EMSCRIPTEN__ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace emscripten; + +// Per-editor entry points and frame probes — defined (with external linkage) in +// pcbnew_embind.cpp / eeschema_embind.cpp. +bool pcbEditorActive(); +void pcbCollabApply( std::string aJson ); +void pcbCollabApplyItems( std::string aJson ); +std::string pcbCollabSnapshot(); +std::string pcbCollabSnapshotItems(); +std::string pcbCollabTestMoveFirst( int aDx, int aDy ); +std::string pcbCollabGetPos( std::string aId ); + +bool schEditorActive(); +void schCollabApply( std::string aJson ); +void schCollabApplyItems( std::string aJson ); +std::string schCollabSnapshot(); +std::string schCollabSnapshotItems(); +std::string schCollabTestMoveFirst( int aDx, int aDy ); +std::string schCollabGetPos( std::string aId ); + + +// Programmatically open a project file in the running editor frame, without UI +// automation. Frame-agnostic (any KIWAY_PLAYER); byte-identical to the definition the +// standalone bundles compile from their own binding TU. +static bool kicadOpenFile( std::string path ) +{ + KIWAY_PLAYER* frame = + wxTheApp ? static_cast( wxTheApp->GetTopWindow() ) : nullptr; + + if( !frame ) + return false; + + if( wxWindow* blocking = frame->Kiway().GetBlockingDialog() ) + blocking->Close( true ); + + return frame->OpenProjectFiles( + std::vector( 1, wxString::FromUTF8( path.c_str() ) ) ); +} + + +// C++ → JS save notification. Called from BOTH fork save chokepoints +// (PCB_EDIT_FRAME::SavePcbFile and SCH_EDIT_FRAME::saveSchematicFile) — one shared +// definition serves the merged image. No-op without a JS listener. +extern "C" void kicadCollabOnSave( const char* aPath ) +{ + EM_ASM( { + if( window.kicadCollab && window.kicadCollab.onSave ) + window.kicadCollab.onSave( UTF8ToString( $0 ) ); + }, aPath ); +} + + +// Dispatch shims: route each shared JS name to the live editor's implementation. +// The sch path is the fallback arm so a JS call with NO frame up behaves like the +// standalone bundles (the per-editor impls no-op / return empty on a null frame). +static void collabApply( std::string aJson ) +{ + pcbEditorActive() ? pcbCollabApply( aJson ) : schCollabApply( aJson ); +} + +static void collabApplyItems( std::string aJson ) +{ + pcbEditorActive() ? pcbCollabApplyItems( aJson ) : schCollabApplyItems( aJson ); +} + +static std::string collabSnapshot() +{ + return pcbEditorActive() ? pcbCollabSnapshot() : schCollabSnapshot(); +} + +static std::string collabSnapshotItems() +{ + return pcbEditorActive() ? pcbCollabSnapshotItems() : schCollabSnapshotItems(); +} + +static std::string collabTestMoveFirst( int aDx, int aDy ) +{ + return pcbEditorActive() ? pcbCollabTestMoveFirst( aDx, aDy ) + : schCollabTestMoveFirst( aDx, aDy ); +} + +static std::string collabGetPos( std::string aId ) +{ + return pcbEditorActive() ? pcbCollabGetPos( aId ) : schCollabGetPos( aId ); +} + + +EMSCRIPTEN_BINDINGS(kicad_editor) { + // Programmatic file open (preferred over UI automation from the web app). + function("kicadOpenFile", &kicadOpenFile); + + // Yjs collaborative bridge entry points — same JS contract as the standalone + // bundles, dispatched on the active editor frame. + function("kicadCollabApply", &collabApply); + function("kicadCollabSnapshot", &collabSnapshot); + function("kicadCollabApplyItems", &collabApplyItems); + function("kicadCollabSnapshotItems", &collabSnapshotItems); + function("kicadCollabTestMoveFirst", &collabTestMoveFirst); + function("kicadCollabGetPos", &collabGetPos); +} + +#endif // __EMSCRIPTEN__ diff --git a/wasm/bindings/pcbnew_embind.cpp b/wasm/bindings/pcbnew_embind.cpp index 8bc2c07..4773511 100644 --- a/wasm/bindings/pcbnew_embind.cpp +++ b/wasm/bindings/pcbnew_embind.cpp @@ -55,6 +55,13 @@ using json = nlohmann::json; // the editor frame is the app's top window and is a KIWAY_PLAYER. Returns the // result of OpenProjectFiles, or false if no frame is available — letting the // JS caller fall back to driving File→Open. +// +// KICAD_MERGED_EMBIND (kicad_editor, editor-unification Part 2): eeschema_embind.cpp +// defines the identical function and registers the same JS names — in the merged image +// the frame-agnostic duplicates (this + kicadCollabOnSave) and the shared-name +// registrations live once in kicad_editor_embind.cpp, which dispatches the per-editor +// entries (renamed pcbCollab*/schCollab* below; JS-facing names are unchanged). +#ifndef KICAD_MERGED_EMBIND bool kicadOpenFile( std::string path ) { KIWAY_PLAYER* frame = @@ -69,6 +76,7 @@ bool kicadOpenFile( std::string path ) return frame->OpenProjectFiles( std::vector( 1, wxString::FromUTF8( path.c_str() ) ) ); } +#endif // !KICAD_MERGED_EMBIND // ───────────────────────────── Yjs collaborative bridge ───────────────────────────── // @@ -912,7 +920,7 @@ void collabTestMove( PCB_EDIT_FRAME* aFrame, BOARD_ITEM* aItem, int aDx, int aDy // fiber stack: BOARD_COMMIT::Push's CHT_ADD of a freshly-built item dispatches GAL virtuals // (view->Add → ViewGetLayers) through asyncify-instrumented invoke_*; off the fiber stack those // mis-dispatch and trap inside KiCad core, on it they dispatch correctly (eeschema 0007). -void kicadCollabApply( std::string aJson ) +void pcbCollabApply( std::string aJson ) { json delta = json::parse( aJson, nullptr, /*allow_exceptions*/ false ); @@ -937,7 +945,7 @@ void kicadCollabApply( std::string aJson ) // JS → C++, v2 items wire. Same CallAfter + COROUTINE context as kicadCollabApply // (the blob parse + commit must run where native edits run — see above). -void kicadCollabApplyItems( std::string aJson ) +void pcbCollabApplyItems( std::string aJson ) { json wire = json::parse( aJson, nullptr, /*allow_exceptions*/ false ); @@ -962,7 +970,7 @@ void kicadCollabApplyItems( std::string aJson ) // JS pull of the full current model as an all-"added" delta (seed/baseline). Also registers the // change listener on first call. -std::string kicadCollabSnapshot() +std::string pcbCollabSnapshot() { BOARD* board = ensureBridge(); @@ -985,7 +993,7 @@ std::string kicadCollabSnapshot() // JS pull of the full current model as an all-"added" v2 items wire: one blob per ROOT // item (a footprint's blob embeds its children — the TS side flattens). Registers the // listener + rebaselines exactly like kicadCollabSnapshot. -std::string kicadCollabSnapshotItems() +std::string pcbCollabSnapshotItems() { BOARD* board = ensureBridge(); @@ -1020,6 +1028,9 @@ std::string kicadCollabSnapshotItems() // kicad fork's save chokepoint (PCB_EDIT_FRAME::SavePcbFile) after a successful // write to MEMFS, so the web app can route the saved bytes onward (API upload, // local-disk write-back, download). No-op without a JS listener. +// KICAD_MERGED_EMBIND: identical definition in eeschema_embind.cpp; the merged image +// gets the one in kicad_editor_embind.cpp (both fork save chokepoints call it). +#ifndef KICAD_MERGED_EMBIND extern "C" void kicadCollabOnSave( const char* aPath ) { EM_ASM( { @@ -1027,6 +1038,15 @@ extern "C" void kicadCollabOnSave( const char* aPath ) window.kicadCollab.onSave( UTF8ToString( $0 ) ); }, aPath ); } +#endif // !KICAD_MERGED_EMBIND + +// Merged-image dispatch probe (kicad_editor_embind.cpp): is the active top window the +// PCB editor? Each shared JS entry routes to the pcb*/sch* implementation whose frame +// is live — exactly the null-check its body starts with anyway. +bool pcbEditorActive() +{ + return pcbFrame() != nullptr; +} void kicadSaveBoard( std::string path ) @@ -1057,7 +1077,7 @@ void kicadSaveBoard( std::string path ) // Test/PoC helper: move the first top-level board item by (dx,dy) IU via a real BOARD_COMMIT, // firing the listener — a deterministic local edit for the two-tab demo / e2e. Returns the // moved item's uuid. -std::string kicadCollabTestMoveFirst( int aDx, int aDy ) +std::string pcbCollabTestMoveFirst( int aDx, int aDy ) { PCB_EDIT_FRAME* fr = pcbFrame(); @@ -1090,7 +1110,7 @@ std::string kicadCollabTestMoveFirst( int aDx, int aDy ) // Test helper: read an item's position by uuid as "x,y" (internal units). -std::string kicadCollabGetPos( std::string aId ) +std::string pcbCollabGetPos( std::string aId ) { PCB_EDIT_FRAME* fr = pcbFrame(); @@ -1194,19 +1214,24 @@ EMSCRIPTEN_BINDINGS(pcbnew) { function("Pad_GetNumber", &Pad_GetNumber, allow_raw_pointers()); function("Pad_GetPinFunction", &Pad_GetPinFunction, allow_raw_pointers()); - // Programmatic file open (preferred over UI automation from the web app). - function("kicadOpenFile", &kicadOpenFile); // Programmatic save of the in-memory board (round-trip tests, README §A). function("kicadSaveBoard", &kicadSaveBoard); - - // Yjs collaborative bridge entry points (same contract as pl_editor / eeschema). - function("kicadCollabApply", &kicadCollabApply); - function("kicadCollabSnapshot", &kicadCollabSnapshot); - // v2 items bridge: per-item s-expr payloads (ysync 0008). - function("kicadCollabApplyItems", &kicadCollabApplyItems); - function("kicadCollabSnapshotItems", &kicadCollabSnapshotItems); - function("kicadCollabTestMoveFirst", &kicadCollabTestMoveFirst); - function("kicadCollabGetPos", &kicadCollabGetPos); + // pcbnew-only test helper (no eeschema counterpart — name is not shared). function("kicadCollabTestItemBlob", &kicadCollabTestItemBlob); + +#ifndef KICAD_MERGED_EMBIND + // JS names ALSO registered by eeschema_embind.cpp — in the merged image these are + // registered once by kicad_editor_embind.cpp, dispatching on the active frame. + // Programmatic file open (preferred over UI automation from the web app). + function("kicadOpenFile", &kicadOpenFile); + // Yjs collaborative bridge entry points (same contract as pl_editor / eeschema). + function("kicadCollabApply", &pcbCollabApply); + function("kicadCollabSnapshot", &pcbCollabSnapshot); + // v2 items bridge: per-item s-expr payloads (ysync 0008). + function("kicadCollabApplyItems", &pcbCollabApplyItems); + function("kicadCollabSnapshotItems", &pcbCollabSnapshotItems); + function("kicadCollabTestMoveFirst", &pcbCollabTestMoveFirst); + function("kicadCollabGetPos", &pcbCollabGetPos); +#endif // !KICAD_MERGED_EMBIND } #endif diff --git a/wasm/editor/CMakeLists.txt b/wasm/editor/CMakeLists.txt new file mode 100644 index 0000000..5365807 --- /dev/null +++ b/wasm/editor/CMakeLists.txt @@ -0,0 +1,57 @@ +# Merged pcbnew+eeschema WASM editor (editor-unification Part 2). +# +# ONE executable links BOTH kifaces; the editor frame (PCB / Footprint / Schematic / +# Symbol) is chosen at runtime by single_top.cpp's --frame flag. Added from the kicad +# fork's top-level CMakeLists.txt via add_subdirectory( ${KICAD_WASM_LAYER}/editor ) +# when EMSCRIPTEN AND KICAD_WASM_MERGED_EDITOR — after eeschema/ and pcbnew/, so both +# kifaces' CACHE INTERNAL library lists exist here. +# +# Collision handling (see KICAD_WASM_PCB_SIDE_RENAMES in the fork's top-level +# CMakeLists.txt and docs/features/editor-unification/): the PCB-side targets compile +# with per-engine symbol renames; each kiface gets its own KIFACE_GETTER name; the +# plain Kiface() the shared common/ code falls back to is defined here +# (merged_kiface_dispatch.cpp). +# +# The standalone pcbnew/eeschema targets still exist in this (option-ON) tree but are +# not intended to be built here: their kiface objects carry the merged renames, and +# their single_top TUs still bind the un-renamed KIFACE_GETTER — build them from their +# own (option-OFF) per-app trees as before. + +include_directories( BEFORE ${INC_BEFORE} ) +include_directories( + ${CMAKE_SOURCE_DIR}/common + ${CMAKE_SOURCE_DIR}/common/dialogs + ${INC_AFTER} + ) + +add_executable( kicad_editor + ${CMAKE_SOURCE_DIR}/common/single_top.cpp + merged_kiface_dispatch.cpp + ) + +# TOP_FRAME is only the DEFAULT frame (no --frame given). No PGM_DATA_FILE_EXT: the +# runtime extension map in single_top.cpp covers all four frames. No BUILD_KIWAY_DLL: +# static kiface linking (matches the per-app WASM executables). KICAD_MERGED_KIFACES +# selects single_top.cpp's dual-getter registration branch. +set_source_files_properties( ${CMAKE_SOURCE_DIR}/common/single_top.cpp PROPERTIES + COMPILE_DEFINITIONS "TOP_FRAME=FRAME_PCB_EDITOR;KICAD_MERGED_KIFACES" + ) + +# Both kifaces' full library sets (exported CACHE INTERNAL by pcbnew/ and eeschema/). +# The overlap (common, kicommon, gal, ...) is deduped; generator-expression entries +# (native-only pads_common/pcm) are inert strings that evaluate to nothing here. +set( KICAD_EDITOR_LIBS ${PCBNEW_KIFACE_LIBRARIES} ${EESCHEMA_KIFACE_LIBRARIES} ) +list( REMOVE_DUPLICATES KICAD_EDITOR_LIBS ) + +target_link_libraries( kicad_editor PRIVATE ${KICAD_EDITOR_LIBS} ) + +# Same link shape as the standalone pcbnew WASM executable: allow-multiple-definition +# for the wx/KiCad dupes, and --whole-archive pcbcommon so RTTI-only-referenced +# vtables/typeinfo are pulled in. +target_link_options( kicad_editor PRIVATE + "LINKER:--allow-multiple-definition" + "LINKER:--whole-archive" + "$" + "LINKER:--no-whole-archive" + $<$:-Wl,--cref,-Map=kicad_editor.map> + ) diff --git a/wasm/editor/merged_kiface_dispatch.cpp b/wasm/editor/merged_kiface_dispatch.cpp new file mode 100644 index 0000000..b231e7b --- /dev/null +++ b/wasm/editor/merged_kiface_dispatch.cpp @@ -0,0 +1,68 @@ +/* + * Merged-editor safety-net Kiface() (editor-unification Part 2). + * + * In the merged kicad_editor image the pcbnew and eeschema kifaces are compiled with + * -DKiface=PcbKiface / -DKiface=SchKiface, so every module-owned call site binds + * statically to its own engine. The handful of shared common/ call sites are patched + * to resolve their owning frame's kiface exactly (eda_base_frame.cpp, + * dialog_color_picker.cpp, design_block_tree_model_adapter.cpp) and only FALL BACK to + * the plain Kiface() — which, in this image, is THIS definition. It also keeps any + * future upstream common/ caller linking. + * + * Resolution is best-effort by design: focused window → top window → KIWAY top frame, + * walking parents to an EDA_BASE_FRAME whose FRAME_T names its face. With the one- + * frame-per-page-load model there is exactly one editor frame, so the walk is exact + * in practice; FACE_PCB is the last-resort default. + */ + +#ifdef __EMSCRIPTEN__ + +#include +#include + +#include +#include +#include + +// single_top.cpp's process-global KIWAY (declared in kiway.h). +static KIFACE_BASE* frameKiface( wxWindow* aWindow ) +{ + for( wxWindow* w = aWindow; w; w = w->GetParent() ) + { + if( EDA_BASE_FRAME* frame = dynamic_cast( w ) ) + { + KIWAY::FACE_T face = KIWAY::KifaceType( frame->GetFrameType() ); + + if( face != KIWAY::FACE_T( -1 ) ) + { + if( KIFACE* kiface = Kiway.KiFACE( face ) ) + return static_cast( kiface ); + } + } + } + + return nullptr; +} + + +KIFACE_BASE& Kiface() +{ + if( KIFACE_BASE* kiface = frameKiface( wxWindow::FindFocus() ) ) + return *kiface; + + if( wxTheApp ) + { + if( KIFACE_BASE* kiface = frameKiface( wxTheApp->GetTopWindow() ) ) + return *kiface; + } + + if( KIFACE_BASE* kiface = frameKiface( Kiway.GetTop() ) ) + return *kiface; + + // Last resort — no frame up yet. FACE_PCB is the merged image's default TOP_FRAME + // face; both faces are registered in OnPgmInit, so this cannot be reached before + // registration in any path that previously had a working Kiface(). + return *static_cast( Kiway.KiFACE( KIWAY::FACE_PCB ) ); +} + +#endif // __EMSCRIPTEN__ diff --git a/web/standalone/src/wasm/boot.ts b/web/standalone/src/wasm/boot.ts index 9cbc926..53f4b78 100644 --- a/web/standalone/src/wasm/boot.ts +++ b/web/standalone/src/wasm/boot.ts @@ -8,6 +8,7 @@ import { TOOL_BUNDLE, TOOL_LIB_KIND, TOOL_NEEDS_CONFIG_SEED, + type Bundle, } from "./constants"; import { installModel3dHandler } from "./libs/models-bridge"; import type { Model3dSource } from "./libs/models-source"; @@ -125,10 +126,10 @@ function loadScript(src: string): Promise { */ function pthreadWorkerScript( base: string, - tool: Tool, + bundle: Bundle, traceMask?: string | null, ): string | Blob { - const abs = new URL(`${base}/${tool}.js`, window.location.href); + const abs = new URL(`${base}/${bundle}.js`, window.location.href); // ?trace=: seed `self.__KICAD_TRACE__` in EVERY pthread worker's scope // before it importScripts the glue. With PROXY_TO_PTHREAD the C main()/UI (and // thus TRACE_MANAGER) run on a pthread, so the trace env must be set in the @@ -143,7 +144,7 @@ function pthreadWorkerScript( { type: "text/javascript" }, ); } - if (abs.origin === window.location.origin) return `${base}/${tool}.js`; + if (abs.origin === window.location.origin) return `${base}/${bundle}.js`; return new Blob([`importScripts(${JSON.stringify(abs.href)});`], { type: "text/javascript", }); @@ -204,7 +205,7 @@ async function doBoot(opts: BootOptions): Promise { // The deployed bundle backing this tool. footprint_editor/symbol_editor share // the pcbnew/eeschema engine, so their `.wasm`/`.js`/pthread-worker files are the // parent's; `tool` still drives identity (thisProgram), config-seed and lib-kind. - const bundle = TOOL_BUNDLE[tool] ?? tool; + const bundle = TOOL_BUNDLE[tool]; const w = window as ToolWindow; // The wasm reads the top-level frame geometry from a GLOBAL `mainWindow` diff --git a/web/standalone/src/wasm/constants.ts b/web/standalone/src/wasm/constants.ts index 91195dd..a953e52 100644 --- a/web/standalone/src/wasm/constants.ts +++ b/web/standalone/src/wasm/constants.ts @@ -35,20 +35,32 @@ export const TOOL_ARGV0: Record = { }; /** - * Which deployed WASM bundle actually backs each tool. `footprint_editor` and - * `symbol_editor` are the SAME compiled engine as `pcbnew` / `eeschema` — they - * were only ever a second launcher pinned to a different build-time frame — so - * after editor-unification they load the parent bundle and select their frame at - * runtime (see `TOOL_FRAME`). Every other tool backs its own bundle. Used to - * resolve the CDN asset folder and the `.{wasm,js}` filenames. + * A deployed WASM bundle (CDN folder + `.{wasm,js}` basenames). NOT the same + * space as `Tool`: since editor-unification Part 2 the four editor TOOLS (pcbnew, + * eeschema, footprint_editor, symbol_editor) are all served by the ONE merged + * `kicad_editor` bundle — two engines (kifaces) statically linked, the frame chosen + * at runtime (`TOOL_FRAME`). Deliberately not part of the `TOOLS` enum: a bundle is + * a delivery artifact, not a user-facing tool/route. */ -export const TOOL_BUNDLE: Record = { - pcbnew: "pcbnew", - eeschema: "eeschema", +export type Bundle = + | "kicad_editor" + | "calculator" + | "pl_editor" + | "gerbview"; + +/** + * Which deployed WASM bundle actually backs each tool. The four editors share the + * merged `kicad_editor` engine image (editor-unification Part 2); the remaining + * tools are genuinely separate engines and back their own bundles. Used to resolve + * the CDN asset folder and the `.{wasm,js}` filenames. + */ +export const TOOL_BUNDLE: Record = { + pcbnew: "kicad_editor", + eeschema: "kicad_editor", calculator: "calculator", pl_editor: "pl_editor", - symbol_editor: "eeschema", - footprint_editor: "pcbnew", + symbol_editor: "kicad_editor", + footprint_editor: "kicad_editor", gerbview: "gerbview", }; @@ -58,10 +70,13 @@ export const TOOL_BUNDLE: Record = { * opens a specific editor frame. Tools whose bundle already defaults to the right * frame need no token (`undefined`). Tokens mirror `kicad/kicad.cpp`'s `--frame` * parser, plus `symedit` for the symbol editor (which upstream's CLI lacks). + * The merged bundle's build-time default is the PCB editor, so every editor tool + * passes its token explicitly (pcbnew included — cheap insurance over relying on + * the default). */ export const TOOL_FRAME: Record = { - pcbnew: undefined, - eeschema: undefined, + pcbnew: "pcb", + eeschema: "sch", calculator: undefined, pl_editor: undefined, symbol_editor: "symedit", diff --git a/web/standalone/src/wasm/wasm-assets.ts b/web/standalone/src/wasm/wasm-assets.ts index e09a50d..bbc1cf6 100644 --- a/web/standalone/src/wasm/wasm-assets.ts +++ b/web/standalone/src/wasm/wasm-assets.ts @@ -43,10 +43,10 @@ export async function resolveWasmBase( ): Promise { if (override) return override.replace(/\/+$/, ""); if (!WASM_MANIFEST_FILE) return WASM_ROOT; // flat (dev / same-origin) - // A tool may be served by a shared bundle (footprint_editor→pcbnew, - // symbol_editor→eeschema); resolve the folder/version of the bundle, not the - // logical tool (the merged bundles are the only ones published). - const bundle = TOOL_BUNDLE[tool] ?? tool; + // A tool may be served by a shared bundle (all four editors → kicad_editor); + // resolve the folder/version of the bundle, not the logical tool (bundles are + // the only thing published/listed in the manifest). + const bundle = TOOL_BUNDLE[tool]; const manifest = await loadManifest(); const ver = manifest.tools?.[bundle]; if (!ver) {