libs 0017: sync overrides indexed + stale-lib session menu + Cmd+S DOM-focus fix + WasmTool split

- kicadLibsFootprintUsage + kicadUpdateFromLibrary embinds (result via
  pcbjam:lib-update-done — runOnCoroutine is deferred)
- standalone: stale-lib FAB triangle + session-menu Update-from-library row,
  save busy notice names the item, footprint placed-usage in the toast
- WasmTool.tsx split: module helpers → components/wasm-tool/
- specs: save-cmd-key (Meta+S, mac UA), fpedit-cmd-save (DOM-focus repro)
- wxwidgets → cdd5a5c (wxDomBlurActive)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Wd1r3ewftpV1DBSEArpRa
This commit is contained in:
Gergő Törcsvári 2026-08-25 17:58:54 +02:00
commit 866db5888c
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
18 changed files with 2084 additions and 1092 deletions

View file

@ -1272,12 +1272,137 @@ std::string schCollabTestMoveFirst( int aDx, int aDy )
}
// Outcome of a deferred update-from-library run → window event
// `pcbjam:lib-update-done` {ok, updated, missing[]} (libs 0017 §2c).
static void emitLibUpdateDone( int aUpdated, const std::vector<std::string>& aMissing )
{
nlohmann::json j;
j["ok"] = true;
j["updated"] = aUpdated;
j["missing"] = aMissing;
std::string s = j.dump();
EM_ASM( {
if( typeof window !== 'undefined' )
window.dispatchEvent( new CustomEvent( 'pcbjam:lib-update-done',
{ detail: JSON.parse( UTF8ToString( $0 ) ) } ) );
}, s.c_str() );
}
// How many placed instances of a library symbol the open schematic holds —
// the JS lib-sync bridge asks after a remote lib update so the editor chrome
// can warn "a symbol you are using changed" (placed SCH_SYMBOLs keep their
// embedded copy across a lib reload, so the user must update explicitly).
// Counts across all unique screens of the hierarchy; 0 without a schematic
// frame (symbol editor / viewer sessions).
// Re-read the named library symbols into every placed instance — the headless
// core of Tools ▸ Update Symbols from Library… (DIALOG_CHANGE_SYMBOLS::
// processSymbols with the dialog's defaults: keep field text/positions, take
// the new body/pins/attributes), scoped to `aNamesJson` of `aLibNickname`.
// Runs as a normal SCH_COMMIT on the frame's coroutine so peers receive it as
// an ordinary edit. Returns {ok, updated, missing[]}. (libs 0017 §2c)
std::string schUpdateFromLibrary( std::string aLibNickname, std::string aNamesJson )
{
nlohmann::json out;
SCH_EDIT_FRAME* fr = schFrame();
if( !fr )
{
out["ok"] = false;
out["error"] = "no schematic frame";
return out.dump();
}
std::set<wxString> names;
try
{
for( const auto& n : nlohmann::json::parse( aNamesJson ) )
names.insert( wxString::FromUTF8( n.get<std::string>().c_str() ) );
}
catch( ... )
{
out["ok"] = false;
out["error"] = "bad names";
return out.dump();
}
const wxString lib = wxString::FromUTF8( aLibNickname.c_str() );
pcbjam_collab::runOnCoroutine( fr, [fr, lib, names]()
{
int updated = 0;
std::vector<std::string> missing;
SCH_COMMIT commit( fr );
SCH_SCREENS screens( fr->Schematic().Root() );
for( SCH_SCREEN* screen = screens.GetFirst(); screen; screen = screens.GetNext() )
{
std::vector<SCH_SYMBOL*> targets;
for( SCH_ITEM* item : screen->Items().OfType( SCH_SYMBOL_T ) )
{
SCH_SYMBOL* sym = static_cast<SCH_SYMBOL*>( item );
const LIB_ID& id = sym->GetLibId();
if( wxString( id.GetLibNickname() ) == lib && names.count( wxString( id.GetLibItemName() ) ) )
targets.push_back( sym );
}
for( SCH_SYMBOL* sym : targets )
{
LIB_SYMBOL* libSymbol = fr->GetLibSymbol( sym->GetLibId() );
if( !libSymbol )
{
missing.push_back( std::string( sym->GetLibId().Format().c_str() ) );
continue;
}
std::unique_ptr<LIB_SYMBOL> flattened = libSymbol->Flatten();
if( flattened->GetUnitCount() < sym->GetUnit() )
{
missing.push_back( std::string( sym->GetLibId().Format().c_str() ) );
continue;
}
// Same order as the dialog: remove, record, swap the lib symbol,
// re-append (the screen's RTree + connectivity re-index).
screen->Remove( sym );
commit.Modified( sym, static_cast<SCH_SYMBOL*>( sym->Clone() ), screen );
sym->SetLibSymbol( flattened.release() );
sym->SetExcludedFromSim( sym->GetLibSymbolRef()->GetExcludedFromSim() );
sym->SetExcludedFromBOM( sym->GetLibSymbolRef()->GetExcludedFromBOM() );
sym->SetExcludedFromBoard( sym->GetLibSymbolRef()->GetExcludedFromBoard() );
sym->SetShowPinNames( sym->GetLibSymbolRef()->GetShowPinNames() );
sym->SetShowPinNumbers( sym->GetLibSymbolRef()->GetShowPinNumbers() );
sym->SetSchSymbolLibraryName( wxEmptyString );
screen->Append( sym );
updated++;
}
}
commit.Push( wxT( "Update symbols from library" ) );
fr->GetCanvas()->Refresh();
emitLibUpdateDone( updated, missing );
} );
// The body runs deferred on the frame's coroutine (runOnCoroutine =
// CallAfter): the caller awaits the `pcbjam:lib-update-done` window event
// for the outcome.
out["ok"] = true;
out["queued"] = true;
return out.dump();
}
// Standalone-eeschema shape of kicadUpdateFromLibrary(kind, lib, namesJson).
static std::string schUpdateFromLibraryShim( std::string aKind, std::string aLib, std::string aNames )
{
if( aKind != "symbol" )
return "{\"ok\":false,\"error\":\"kind not handled by this editor\"}";
return schUpdateFromLibrary( aLib, aNames );
}
int schLibsSymbolUsage( std::string aLibNickname, std::string aSymbolName )
{
SCH_EDIT_FRAME* fr = schFrame();
@ -2110,6 +2235,8 @@ EMSCRIPTEN_BINDINGS(eeschema) {
// Placed-instance count for a library symbol (drives the "a symbol you are
// using was updated" toast after a remote lib edit).
function("kicadLibsSymbolUsage", &schLibsSymbolUsage);
// Update placed symbols from the library (libs 0017 §2c).
function("kicadUpdateFromLibrary", &schUpdateFromLibraryShim);
#endif // !KICAD_MERGED_EMBIND
}
#endif

View file

@ -50,6 +50,10 @@ using namespace emscripten;
// Per-editor entry points and frame probes — defined (with external linkage) in
// pcbnew_embind.cpp / eeschema_embind.cpp.
bool pcbEditorActive();
// libs 0017 §2c/2d: placed-footprint usage + update-from-library.
int pcbLibsFootprintUsage( 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 );
void pcbCollabApplyItems( std::string aJson );
std::string pcbCollabSnapshot();
@ -409,6 +413,26 @@ static int libsSymbolUsage( std::string aLib, std::string aName )
return schEditorActive() ? schLibsSymbolUsage( aLib, aName ) : 0;
}
// Placed-instance count for a library footprint — board frame only (libs 0017 §2d).
static int libsFootprintUsage( std::string aLib, std::string aName )
{
return pcbEditorActive() ? pcbLibsFootprintUsage( aLib, aName ) : 0;
}
// Update placed instances of the named lib items from the library (libs 0017
// §2c): `aKind` picks the editor — "footprint" needs the board frame,
// "symbol" the schematic frame; a mismatch answers {ok:false}.
static std::string updateFromLibrary( std::string aKind, std::string aLib, std::string aNamesJson )
{
if( aKind == "footprint" && pcbEditorActive() )
return pcbUpdateFromLibrary( aLib, aNamesJson );
if( aKind == "symbol" && schEditorActive() )
return schUpdateFromLibrary( aLib, aNamesJson );
return "{\"ok\":false,\"error\":\"no editor for this kind\"}";
}
// Presence shims (collab-presence 0002 pcbnew / 0003 eeschema): route to the live
// editor's implementation, same pattern as the collab bridge shims above.
static void collabPresenceStart()
@ -603,6 +627,9 @@ EMSCRIPTEN_BINDINGS(kicad_editor) {
// 0 from any other frame; drives the "symbol you are using was updated"
// toast after a remote lib edit).
function("kicadLibsSymbolUsage", &libsSymbolUsage);
// Placed-footprint usage + update-from-library (libs 0017 §2c/2d).
function("kicadLibsFootprintUsage", &libsFootprintUsage);
function("kicadUpdateFromLibrary", &updateFromLibrary);
}
#endif // __EMSCRIPTEN__

View file

@ -26,6 +26,7 @@
#include <zone.h>
#include <eda_text.h>
#include <pcb_edit_frame.h>
#include <lib_id.h>
#include <kicad_clipboard.h>
#include <io/kicad/kicad_io_utils.h>
#include <richio.h>
@ -2480,6 +2481,140 @@ static bool kicadCollabBusyProbe()
return pcbjam_collab::applyBusy() || !pcbjam_collab::applyQueue().empty();
}
// ── libs 0017 §2d/§2c: placed-footprint usage + update-from-library ──────────
// 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.
int pcbLibsFootprintUsage( std::string aLibNickname, std::string aFootprintName )
{
PCB_EDIT_FRAME* fr = pcbFrame();
if( !fr || !fr->GetBoard() )
return 0;
const LIB_ID target( wxString::FromUTF8( aLibNickname.c_str() ),
wxString::FromUTF8( aFootprintName.c_str() ) );
int count = 0;
for( FOOTPRINT* fp : fr->GetBoard()->Footprints() )
{
if( fp->GetFPID() == target )
count++;
}
return count;
}
// Outcome of a deferred update-from-library run → window event
// `pcbjam:lib-update-done` {ok, updated, missing[]} (libs 0017 §2c).
static void emitLibUpdateDone( int aUpdated, const std::vector<std::string>& aMissing )
{
nlohmann::json j;
j["ok"] = true;
j["updated"] = aUpdated;
j["missing"] = aMissing;
std::string s = j.dump();
EM_ASM( {
if( typeof window !== 'undefined' )
window.dispatchEvent( new CustomEvent( 'pcbjam:lib-update-done',
{ detail: JSON.parse( UTF8ToString( $0 ) ) } ) );
}, s.c_str() );
}
// Re-read the named library footprints into every placed instance — the
// headless core of Tools ▸ Update Footprints from Library… (DIALOG_EXCHANGE_
// FOOTPRINTS::processFootprint with the dialog's defaults), scoped to
// `aNamesJson` (a JSON array of footprint names) of `aLibNickname`. Runs as a
// normal BOARD_COMMIT on the frame's coroutine so peers receive it as an
// ordinary edit. Returns {ok, updated, missing[]}.
std::string pcbUpdateFromLibrary( std::string aLibNickname, std::string aNamesJson )
{
nlohmann::json out;
PCB_EDIT_FRAME* fr = pcbFrame();
if( !fr || !fr->GetBoard() )
{
out["ok"] = false;
out["error"] = "no board frame";
return out.dump();
}
std::set<wxString> names;
try
{
for( const auto& n : nlohmann::json::parse( aNamesJson ) )
names.insert( wxString::FromUTF8( n.get<std::string>().c_str() ) );
}
catch( ... )
{
out["ok"] = false;
out["error"] = "bad names";
return out.dump();
}
const wxString lib = wxString::FromUTF8( aLibNickname.c_str() );
pcbjam_collab::runOnCoroutine( fr, [fr, lib, names]()
{
int updated = 0;
std::vector<std::string> missing;
BOARD_COMMIT commit( fr );
// Reverse: ExchangeFootprint appends the replacement at the end of the list.
std::vector<FOOTPRINT*> targets;
for( FOOTPRINT* fp : fr->GetBoard()->Footprints() )
{
const LIB_ID& id = fp->GetFPID();
if( wxString( id.GetLibNickname() ) == lib && names.count( wxString( id.GetLibItemName() ) ) )
targets.push_back( fp );
}
for( auto it = targets.rbegin(); it != targets.rend(); ++it )
{
FOOTPRINT* fp = *it;
FOOTPRINT* fresh = fr->LoadFootprint( fp->GetFPID() );
if( !fresh )
{
missing.push_back( std::string( fp->GetFPID().Format().c_str() ) );
continue;
}
bool changed = fp->FootprintNeedsUpdate( fresh );
fr->ExchangeFootprint( fp, fresh, commit, /*deleteExtraTexts*/ true,
/*resetTextLayers*/ true, /*resetTextEffects*/ true,
/*resetTextPositions*/ true, /*resetTextContent*/ true,
/*resetFabricationAttrs*/ true,
/*resetClearanceOverrides*/ true, /*reset3DModels*/ true,
&changed );
updated++;
}
commit.Push( wxT( "Update footprints from library" ) );
fr->GetCanvas()->Refresh();
emitLibUpdateDone( updated, missing );
} );
// The body runs deferred on the frame's coroutine (runOnCoroutine =
// CallAfter): the caller awaits the `pcbjam:lib-update-done` window event
// for the outcome.
out["ok"] = true;
out["queued"] = true;
return out.dump();
}
// Standalone-pcbnew shape of kicadUpdateFromLibrary(kind, lib, namesJson).
static std::string pcbUpdateFromLibraryShim( std::string aKind, std::string aLib, std::string aNames )
{
if( aKind != "footprint" )
return "{\"ok\":false,\"error\":\"kind not handled by this editor\"}";
return pcbUpdateFromLibrary( aLib, aNames );
}
EMSCRIPTEN_BINDINGS(pcbnew) {
// Register vector types for iteration
register_vector<FOOTPRINT*>("FootprintVector");
@ -2497,6 +2632,11 @@ EMSCRIPTEN_BINDINGS(pcbnew) {
// Programmatic save of the in-memory board (round-trip tests, README §A).
function("kicadSaveBoard", &kicadSaveBoard);
#ifndef KICAD_MERGED_EMBIND
// Placed-footprint usage + update-from-library (libs 0017 §2c/2d).
function("kicadLibsFootprintUsage", &pcbLibsFootprintUsage);
function("kicadUpdateFromLibrary", &pcbUpdateFromLibraryShim);
#endif
// Layer bridge (viewer-panels) — pcbnew-only names, merged-image safe
// (null-frame no-op when eeschema is the live frame).
function("kicadLayersGetState", &pcbLayersGetState);