diff --git a/kicad b/kicad index ab62afa..27051b4 160000 --- a/kicad +++ b/kicad @@ -1 +1 @@ -Subproject commit ab62afa4732bf2b4c26cf185e4b08b2912edae01 +Subproject commit 27051b46e2b3662731f913355a2bd752ca5c6561 diff --git a/scripts/kicad/build-kicad-target.sh b/scripts/kicad/build-kicad-target.sh index bf60b52..e06aa76 100755 --- a/scripts/kicad/build-kicad-target.sh +++ b/scripts/kicad/build-kicad-target.sh @@ -602,6 +602,10 @@ 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" + # The editor-frame headers the bindings reach into (libs 0019 F3: the + # Symbol Editor's open copy) live one level down, like eeschema's own + # CMake include list (./symbol_editor). + _includes+=" -I${KICAD_DIR}/${_subdir}/symbol_editor -I${KICAD_DIR}/${_subdir}/widgets" # 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" diff --git a/wasm/bindings/eeschema_embind.cpp b/wasm/bindings/eeschema_embind.cpp index b7b9a6b..dccbfc1 100644 --- a/wasm/bindings/eeschema_embind.cpp +++ b/wasm/bindings/eeschema_embind.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -1383,6 +1384,29 @@ std::string schUpdateFromLibrary( std::string aLibNickname, std::string aNamesJs commit.Push( wxT( "Update symbols from library" ) ); fr->GetCanvas()->Refresh(); + + // The Symbol Editor's own copy (libs 0019 F3): re-open it from the + // fresh library when unmodified; report and leave it with local edits. + if( SYMBOL_EDIT_FRAME* se = dynamic_cast( + fr->Kiway().Player( FRAME_SCH_SYMBOL_EDITOR, false ) ) ) + { + if( LIB_SYMBOL* cur = se->GetCurSymbol() ) + { + const wxString curName = cur->GetName(); + + if( se->GetCurLib() == lib && names.count( curName ) ) + { + const LIB_ID id( lib, curName ); + + if( se->IsContentModified() ) + missing.push_back( std::string( id.Format().c_str() ) + + " (open in the Symbol Editor with unsaved edits — save or revert it first)" ); + else + se->LoadSymbol( id, 1, 1 ); + } + } + } + emitLibUpdateDone( updated, missing ); } ); @@ -1425,6 +1449,18 @@ int schLibsSymbolUsage( std::string aLibNickname, std::string aSymbolName ) } } + // The Symbol Editor's open copy counts as "used" too (libs 0019 F3). + if( SYMBOL_EDIT_FRAME* se = dynamic_cast( + fr->Kiway().Player( FRAME_SCH_SYMBOL_EDITOR, false ) ) ) + { + if( LIB_SYMBOL* cur = se->GetCurSymbol() ) + { + if( se->GetCurLib() == wxString( target.GetLibNickname() ) + && cur->GetName() == wxString( target.GetLibItemName() ) ) + count++; + } + } + return count; } diff --git a/wasm/bindings/kicad_editor_embind.cpp b/wasm/bindings/kicad_editor_embind.cpp index 6a45cdc..9e26438 100644 --- a/wasm/bindings/kicad_editor_embind.cpp +++ b/wasm/bindings/kicad_editor_embind.cpp @@ -52,6 +52,11 @@ using namespace emscripten; bool pcbEditorActive(); // libs 0017 §2c/2d: placed-footprint usage + update-from-library. int pcbLibsFootprintUsage( std::string aLib, std::string aName ); +void pcbLibsInvalidatePreloaded( std::string aLib ); +int pcbLibsTestPreload( std::string aLib ); +std::string pcbLibsTestLoadFootprint( std::string aLib, std::string aName ); +std::string pcbLibsTestEditorFootprint(); +bool pcbLibsTestEditorLoad( std::string aLib, std::string aName ); std::string pcbUpdateFromLibrary( std::string aLib, std::string aNamesJson ); std::string schUpdateFromLibrary( std::string aLib, std::string aNamesJson ); void pcbCollabApply( std::string aJson ); @@ -413,6 +418,47 @@ static int libsSymbolUsage( std::string aLib, std::string aName ) return schEditorActive() ? schLibsSymbolUsage( aLib, aName ) : 0; } +// libs 0019 F2: a remote lib edit only INVALIDATES (cheap: drop the plugin +// entry + pcbnew's preloaded footprint cache); the fat re-load runs lazily on +// the next access or explicitly via kicadLibsReload from "Update from library". +static void libsInvalidate( std::string aKind, std::string aNick ) +{ + if( aKind == "footprint" && pcbEditorActive() ) + pcbLibsInvalidatePreloaded( aNick ); + + pcbjam_libs::invalidateLibrary( aKind, aNick ); +} + +// Full refresh: the preloaded cache must go too, or the re-loaded plugin sits +// under stale parsed copies (libs 0019 F1). +static void libsReload( std::string aKind, std::string aNick ) +{ + if( aKind == "footprint" && pcbEditorActive() ) + pcbLibsInvalidatePreloaded( aNick ); + + pcbjam_libs::reloadLibrary( aKind, aNick ); +} + +static int libsTestPreload( std::string aLib ) +{ + return pcbEditorActive() ? pcbLibsTestPreload( aLib ) : -1; +} + +static std::string libsTestLoadFootprint( std::string aLib, std::string aName ) +{ + return pcbEditorActive() ? pcbLibsTestLoadFootprint( aLib, aName ) : ""; +} + +static std::string libsTestEditorFootprint() +{ + return pcbEditorActive() ? pcbLibsTestEditorFootprint() : ""; +} + +static bool libsTestEditorLoad( std::string aLib, std::string aName ) +{ + return pcbEditorActive() ? pcbLibsTestEditorLoad( aLib, aName ) : false; +} + // Placed-instance count for a library footprint — board frame only (libs 0017 §2d). static int libsFootprintUsage( std::string aLib, std::string aName ) { @@ -619,7 +665,13 @@ EMSCRIPTEN_BINDINGS(kicad_editor) { function("kicadCollabTestSelectFirst", &collabTestSelectFirst); function("kicadCollabTestClearSelection", &collabTestClearSelection); // Library reload after a remote (synced) lib edit — r2-idb-sync realtime. - function("kicadLibsReload", &pcbjam_libs::reloadLibrary PCBJAM_PARKER_POLICY); + function("kicadLibsReload", &libsReload PCBJAM_PARKER_POLICY); + // Cheap invalidation for remote edits (libs 0019 F2) + smoke probes. + function("kicadLibsInvalidate", &libsInvalidate PCBJAM_PARKER_POLICY); + function("kicadLibsTestPreload", &libsTestPreload PCBJAM_PARKER_POLICY); + function("kicadLibsTestLoadFootprint", &libsTestLoadFootprint PCBJAM_PARKER_POLICY); + function("kicadLibsTestEditorFootprint", &libsTestEditorFootprint); + function("kicadLibsTestEditorLoad", &libsTestEditorLoad); // Runtime lib-table row insert + load (a new team library appeared // mid-session; the lib set is otherwise frozen at boot). function("kicadLibsAddEntry", &pcbjam_libs::addLibraryEntry PCBJAM_PARKER_POLICY); diff --git a/wasm/bindings/pcbjam_libs_reload.h b/wasm/bindings/pcbjam_libs_reload.h index d30eb2e..8cb200c 100644 --- a/wasm/bindings/pcbjam_libs_reload.h +++ b/wasm/bindings/pcbjam_libs_reload.h @@ -77,6 +77,33 @@ inline void reloadLibrary( std::string aKind, std::string aNickname ) } ); } +/** + * Cheap counterpart of reloadLibrary (libs 0019): drop the lib's LIB_DATA + * entry (plugin instance + parsed cache) so the NEXT access re-reads the + * provider — no eager LoadLibraryEntry fat-load, no tree mail. A remote edit + * calls this; the fat-load happens lazily when the user looks at the lib, + * or explicitly through reloadLibrary from "Update from library". + * (pcbnew's FOOTPRINT_LIBRARY_ADAPTER::PreloadedFootprints is invalidated by + * the pcbnew-side caller — this header stays common-code only.) + */ +inline void invalidateLibrary( std::string aKind, std::string aNickname ) +{ + KIWAY_PLAYER* top = + wxTheApp ? dynamic_cast( wxTheApp->GetTopWindow() ) : nullptr; + + if( !top ) + return; + + const bool fp = aKind == "footprint"; + const wxString nick = wxString::FromUTF8( aNickname.c_str() ); + + pcbjam_collab::runOnCoroutine( top, [fp, nick]() + { + Pgm().GetLibraryManager().ReloadLibraryEntry( + fp ? LIBRARY_TABLE_TYPE::FOOTPRINT : LIBRARY_TABLE_TYPE::SYMBOL, nick ); + } ); +} + /** * Add one PCBJAM lib-table row at RUNTIME and load it — the lib SET is * otherwise frozen at boot (sym/fp-lib-table are written once in preRun). diff --git a/wasm/bindings/pcbnew_embind.cpp b/wasm/bindings/pcbnew_embind.cpp index c45335c..a3d612d 100644 --- a/wasm/bindings/pcbnew_embind.cpp +++ b/wasm/bindings/pcbnew_embind.cpp @@ -26,6 +26,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -2484,9 +2487,129 @@ static bool kicadCollabBusyProbe() // ── libs 0017 §2d/§2c: placed-footprint usage + update-from-library ────────── +// The Footprint Editor frame opened from this board session (nullptr when +// none exists — Player(…, false) never creates one). +static FOOTPRINT_EDIT_FRAME* fpEditorFrame( PCB_EDIT_FRAME* aFrame ) +{ + if( !aFrame ) + return nullptr; + + return dynamic_cast( + aFrame->Kiway().Player( FRAME_FOOTPRINT_EDITOR, false ) ); +} + +// libs 0019 F1: drop pcbnew's SECOND footprint cache for one library. +// FOOTPRINT_LIBRARY_ADAPTER::PreloadedFootprints is read before the plugin by +// the tree, the preview and LoadFootprint, and its only invalidation is +// timestamp-gated (PCB_IO_PCBJAM_FP pins the timestamp), so a remote edit +// never reached it — the tree/preview/update kept serving the old body. +void pcbLibsInvalidatePreloaded( std::string aLibNickname ) +{ + PCB_EDIT_FRAME* fr = pcbFrame(); + + if( !fr ) + return; + + PROJECT_PCB::FootprintLibAdapter( &fr->Prj() ) + ->InvalidatePreloaded( wxString::FromUTF8( aLibNickname.c_str() ) ); +} + +// Test probes (libs 0019 smoke): warm the adapter's preloaded cache the way the +// footprint editor's tree does, and read a footprint's pad sizes through the +// SAME LoadFootprint path the preview / update-from-library use. +int pcbLibsTestPreload( std::string aLibNickname ) +{ + PCB_EDIT_FRAME* fr = pcbFrame(); + + if( !fr ) + return -1; + + FOOTPRINT_LIBRARY_ADAPTER* adapter = PROJECT_PCB::FootprintLibAdapter( &fr->Prj() ); + adapter->AsyncLoad(); + adapter->BlockUntilLoaded(); + return (int) adapter->GetFootprints( wxString::FromUTF8( aLibNickname.c_str() ), true ).size(); +} + +std::string pcbLibsTestLoadFootprint( std::string aLibNickname, std::string aFootprintName ) +{ + PCB_EDIT_FRAME* fr = pcbFrame(); + + if( !fr ) + return ""; + + FOOTPRINT_LIBRARY_ADAPTER* adapter = PROJECT_PCB::FootprintLibAdapter( &fr->Prj() ); + std::unique_ptr fp; + + try + { + fp.reset( adapter->LoadFootprint( wxString::FromUTF8( aLibNickname.c_str() ), + wxString::FromUTF8( aFootprintName.c_str() ), false ) ); + } + catch( ... ) + { + return ""; + } + + if( !fp ) + return ""; + + std::string out; + + for( PAD* pad : fp->Pads() ) + { + out += std::string( pad->GetNumber().utf8_str() ) + ":" + + std::to_string( pad->GetSize( PADSTACK::ALL_LAYERS ).x ) + "x" + + std::to_string( pad->GetSize( PADSTACK::ALL_LAYERS ).y ) + ";"; + } + + return out; +} + +// Smoke plumbing: load a library footprint into the (already open) Footprint +// Editor frame — what a tree double-click does, without tree geometry. +bool pcbLibsTestEditorLoad( std::string aLibNickname, std::string aFootprintName ) +{ + FOOTPRINT_EDIT_FRAME* fe = fpEditorFrame( pcbFrame() ); + + if( !fe ) + return false; + + const LIB_ID id( wxString::FromUTF8( aLibNickname.c_str() ), + wxString::FromUTF8( aFootprintName.c_str() ) ); + pcbjam_collab::runOnCoroutine( fe, [fe, id]() { fe->LoadFootprintFromLibrary( id ); } ); + return true; +} + +// Smoke probe: the Footprint Editor's OPEN copy — ":|" or "". +std::string pcbLibsTestEditorFootprint() +{ + FOOTPRINT_EDIT_FRAME* fe = fpEditorFrame( pcbFrame() ); + + if( !fe || !fe->GetBoard() ) + return ""; + + FOOTPRINT* fp = fe->GetBoard()->GetFirstFootprint(); + + if( !fp ) + return ""; + + std::string out = std::string( fe->GetLoadedFPID().Format().c_str() ) + "|"; + + for( PAD* pad : fp->Pads() ) + { + out += std::string( pad->GetNumber().utf8_str() ) + ":" + + std::to_string( pad->GetSize( PADSTACK::ALL_LAYERS ).x ) + "x" + + std::to_string( pad->GetSize( PADSTACK::ALL_LAYERS ).y ) + ";"; + } + + return out; +} + // Placed-instance count for a library footprint (mirror of schLibsSymbolUsage): // drives the "a footprint you placed was updated" notice after a remote lib -// edit. 0 without a board frame. +// edit. Counts the board's placements PLUS the copy open in the Footprint +// Editor (libs 0019 F3 — an open-but-unplaced footprint is "used" too). +// 0 without a board frame. int pcbLibsFootprintUsage( std::string aLibNickname, std::string aFootprintName ) { PCB_EDIT_FRAME* fr = pcbFrame(); @@ -2504,6 +2627,12 @@ int pcbLibsFootprintUsage( std::string aLibNickname, std::string aFootprintName count++; } + if( FOOTPRINT_EDIT_FRAME* fe = fpEditorFrame( fr ) ) + { + if( fe->GetLoadedFPID() == target ) + count++; + } + return count; } @@ -2595,6 +2724,26 @@ std::string pcbUpdateFromLibrary( std::string aLibNickname, std::string aNamesJs commit.Push( wxT( "Update footprints from library" ) ); fr->GetCanvas()->Refresh(); + + // The Footprint Editor's own copy (libs 0019 F3): re-open it from the + // (fresh) library when it is one of the named items and has no local + // edits; with edits pending it is left alone and reported, never + // clobbered. + if( FOOTPRINT_EDIT_FRAME* fe = fpEditorFrame( fr ) ) + { + const LIB_ID loaded = fe->GetLoadedFPID(); + + if( wxString( loaded.GetLibNickname() ) == lib + && names.count( wxString( loaded.GetLibItemName() ) ) ) + { + if( fe->IsContentModified() ) + missing.push_back( std::string( loaded.Format().c_str() ) + + " (open in the Footprint Editor with unsaved edits — save or revert it first)" ); + else + fe->LoadFootprintFromLibrary( loaded ); + } + } + emitLibUpdateDone( updated, missing ); } ); @@ -2636,6 +2785,10 @@ EMSCRIPTEN_BINDINGS(pcbnew) { // Placed-footprint usage + update-from-library (libs 0017 §2c/2d). function("kicadLibsFootprintUsage", &pcbLibsFootprintUsage); function("kicadUpdateFromLibrary", &pcbUpdateFromLibraryShim); + function("kicadLibsTestPreload", &pcbLibsTestPreload); + function("kicadLibsTestLoadFootprint", &pcbLibsTestLoadFootprint); + function("kicadLibsTestEditorFootprint", &pcbLibsTestEditorFootprint); + function("kicadLibsTestEditorLoad", &pcbLibsTestEditorLoad); #endif // Layer bridge (viewer-panels) — pcbnew-only names, merged-image safe // (null-frame no-op when eeschema is the live frame). diff --git a/web/standalone/src/components/wasm-tool/useLibNotices.ts b/web/standalone/src/components/wasm-tool/useLibNotices.ts index 53dc626..fb94c02 100644 --- a/web/standalone/src/components/wasm-tool/useLibNotices.ts +++ b/web/standalone/src/components/wasm-tool/useLibNotices.ts @@ -90,8 +90,11 @@ export function useLibNotices(opts: { }, []); /** Update every placed instance of the stale items from the library (2c). */ const updateStaleFromLibrary = React.useCallback(async () => { - const mod = (window as { Module?: { kicadUpdateFromLibrary?: unknown } }).Module; + const mod = (window as { + Module?: { kicadUpdateFromLibrary?: unknown; kicadLibsReload?: unknown }; + }).Module; const fn = mod?.kicadUpdateFromLibrary; + const reload = mod?.kicadLibsReload; if (typeof fn !== "function") { setLibError("This editor build can't update placed items from the library — reload to refresh them."); return; @@ -102,10 +105,20 @@ export function useLibNotices(opts: { // The bridge queues the edit on the frame's coroutine and answers // {queued:true}; the outcome arrives as a `pcbjam:lib-update-done` // window event (or {ok:false,error} synchronously). - const done = new Promise<{ ok: boolean; updated?: number; error?: string }>((resolve) => { + // libs 0019 F2: the remote edit only invalidated the caches — bring the + // lib back to LOADED first (same coroutine queue, so it lands before + // the exchange below). + if (typeof reload === "function") { + try { + (reload as (kind: string, lib: string) => void)(entry.kind, entry.lib); + } catch { + /* the update below falls back to the lazy load */ + } + } + const done = new Promise<{ ok: boolean; updated?: number; missing?: string[]; error?: string }>((resolve) => { const onDone = (e: Event) => { window.removeEventListener("pcbjam:lib-update-done", onDone); - resolve((e as CustomEvent<{ ok: boolean; updated?: number }>).detail); + resolve((e as CustomEvent<{ ok: boolean; updated?: number; missing?: string[] }>).detail); }; window.addEventListener("pcbjam:lib-update-done", onDone); setTimeout(() => { @@ -131,6 +144,10 @@ export function useLibNotices(opts: { continue; } console.log(`[libs] updated ${outcome.updated ?? "?"} placed ${entry.kind}(s) from "${entry.lib}"`); + // An open editor copy with local edits is left alone (0019 F3) — say so. + if (outcome.missing && outcome.missing.length > 0) { + setLibError(`Not updated: ${outcome.missing.join("; ")}`); + } clearStale(key); } } finally { diff --git a/web/standalone/src/wasm/libs/synced-source.ts b/web/standalone/src/wasm/libs/synced-source.ts index 8c7406f..ff4feb4 100644 --- a/web/standalone/src/wasm/libs/synced-source.ts +++ b/web/standalone/src/wasm/libs/synced-source.ts @@ -111,13 +111,21 @@ export function syncedLibsSource( const names = [...(pendingNames.get(kind) ?? [])]; pendingNames.delete(kind); const mod = (globalThis as { Module?: Record }).Module; + // libs 0019 F2: a remote edit only INVALIDATES the editor's caches + // (cheap); the fat re-load happens lazily when the user looks at the + // lib, or explicitly from "Update from library" (kicadLibsReload). + // Older builds without the invalidate export keep the eager reload. + const invalidate = mod?.kicadLibsInvalidate; const reload = mod?.kicadLibsReload; - if (typeof reload !== "function") return; - log(`[synced] remote change → reload ${kind} lib "${info.name}"`); + const fn = typeof invalidate === "function" ? invalidate : reload; + if (typeof fn !== "function") return; + log( + `[synced] remote change → ${fn === invalidate ? "invalidate" : "reload"} ${kind} lib "${info.name}"`, + ); try { - (reload as (kind: string, nickname: string) => void)(kind, info.name); + (fn as (kind: string, nickname: string) => void)(kind, info.name); } catch (e) { - log(`[synced] editor reload failed: ${String(e)}`); + log(`[synced] editor cache invalidation failed: ${String(e)}`); return; } emitItemUpdated(info, kind, names, mod);