feat(collab): cross-app selection — eeschema symbol ⇄ pcbnew footprint ghost highlight (collab-presence 0006)
Selecting a symbol in eeschema ghost-highlights the linked footprint(s) in
every pcbnew tab of the project, and vice versa — across users AND one
user's own two tabs. Native KIWAY cross-probe is inert in WASM (one frame
per page); this rides the presence layer instead.
- cross-app.ts: project-wide awareness-only room (presenceRoomId), publishes
full PresenceState at selection rate (cursor always null); peers() = other-
TOOL clients incl. own user's other tabs; window.__pcbjamCrossApp test handle
- presence-kicad.ts: parseSelectionEmit (bare array | {uuids,fpPaths}),
xselFromPeerState (pcbnew paths → symbol uuids; eeschema uuids verbatim),
cross peers appended to the kicadCollabSetRemote snapshot as
{id "<user>#x<client>", name "<user> · sch|pcb", xsel}
- C++ (zero fork changes): pcbnew emits {uuids, fpPaths} (FOOTPRINT::GetPath)
and ghost-renders xsel via path-tail suffix scan; eeschema resolves xsel via
ResolveItem gated to the CURRENT sheet (xsel arrives project-wide, unlike
room-scoped selections); ghostStyle = alphas × xselAlphaScale (0.55, tuner-
patchable); new exports kicadCollabGetSelectionFull / TestGetCrossMapped /
TestSelectComponent (skips power symbols — PWR_FLAG has no footprint) +
merged-image dispatch
- tests: presence suites extended (payload shape, ghost render pixel tests,
13/13) + new two-tab tests/web/cross-probe.spec.ts (passing vs real
partykit); eeschema pixel compares now target the #glcanvas-* GAL panel
(the whole-window #canvas compare flaked on the auto-dismissing version
infobar — also fixes the long-known presence-eeschema restore flake);
cross-app + presence-kicad vitest suites
Spec: docs/features/collab-presence/0006 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
This commit is contained in:
parent
157d60f6b8
commit
3186986a0a
14 changed files with 1276 additions and 48 deletions
|
|
@ -701,6 +701,10 @@ struct PEER
|
|||
bool hasCursor = false;
|
||||
VECTOR2D cursor; // world coords (IU)
|
||||
std::vector<KIID> selection;
|
||||
// Cross-app selection (0006): SYMBOL uuids derived from a pcbnew peer's
|
||||
// footprint paths (the TS side strips the KIID_PATH down to its tail).
|
||||
// Ghost-rendered when the symbol resolves onto the CURRENT sheet.
|
||||
std::vector<KIID> xsel;
|
||||
};
|
||||
|
||||
// Comment pin dot (collab-presence 0005): the GAL half of the hybrid pin —
|
||||
|
|
@ -839,6 +843,29 @@ void redrawOverlay()
|
|||
g_style );
|
||||
}
|
||||
|
||||
// Cross-app ghosts (0006): a pcbnew peer's selection, as symbol uuids.
|
||||
// Unlike same-room selections (per-sheet rooms scope those by
|
||||
// construction) these arrive project-wide, so only draw items that
|
||||
// resolve onto the sheet THIS canvas is showing — a bbox from another
|
||||
// sheet's screen would land at meaningless coordinates here.
|
||||
if( !peer.xsel.empty() )
|
||||
{
|
||||
pcbjam_presence::STYLE ghost = pcbjam_presence::ghostStyle( g_style );
|
||||
|
||||
for( const KIID& id : peer.xsel )
|
||||
{
|
||||
SCH_SHEET_PATH path;
|
||||
SCH_ITEM* item = fr->Schematic().ResolveItem( id, &path, /*allowNull*/ true );
|
||||
|
||||
if( !item || path.LastScreen() != fr->GetScreen() )
|
||||
continue;
|
||||
|
||||
pcbjam_presence::drawSelectionBox( g_overlay.get(), g_textOverlay.get(),
|
||||
item->ViewBBox(), peer.name, color, px,
|
||||
ghost );
|
||||
}
|
||||
}
|
||||
|
||||
if( peer.hasCursor )
|
||||
pcbjam_presence::drawCursor( g_overlay.get(), g_textOverlay.get(), peer.cursor,
|
||||
peer.name, color, px, g_style );
|
||||
|
|
@ -1596,6 +1623,13 @@ void schCollabSetRemote( std::string aJson )
|
|||
peer.selection.emplace_back( wxString::FromUTF8( u.get<std::string>().c_str() ) );
|
||||
}
|
||||
|
||||
// Cross-app selection (0006): symbol uuids from a pcbnew peer.
|
||||
for( const json& u : p.value( "xsel", json::array() ) )
|
||||
{
|
||||
if( u.is_string() )
|
||||
peer.xsel.emplace_back( wxString::FromUTF8( u.get<std::string>().c_str() ) );
|
||||
}
|
||||
|
||||
peers.push_back( std::move( peer ) );
|
||||
}
|
||||
|
||||
|
|
@ -1776,6 +1810,48 @@ std::string schCollabGetSelection()
|
|||
return presence::selectionUuids( fr ).dump();
|
||||
}
|
||||
|
||||
// JS pull of the current selection in the 0006 payload shape. eeschema has no
|
||||
// footprint paths — the uuids ARE the symbol uuids — but the export keeps the
|
||||
// merged image's kicadCollabGetSelectionFull contract uniform across editors.
|
||||
std::string schCollabGetSelectionFull()
|
||||
{
|
||||
SCH_EDIT_FRAME* fr = schFrame();
|
||||
|
||||
if( !fr )
|
||||
return "{\"uuids\":[],\"fpPaths\":[]}";
|
||||
|
||||
json payload;
|
||||
payload["uuids"] = presence::selectionUuids( fr );
|
||||
payload["fpPaths"] = json::array();
|
||||
return payload.dump();
|
||||
}
|
||||
|
||||
// Test probe (0006): the schematic-item uuids the current peers' cross-app
|
||||
// selections resolve to ON THE CURRENT SHEET (mirrors the redraw's gate).
|
||||
std::string schCollabTestGetCrossMapped()
|
||||
{
|
||||
json arr = json::array();
|
||||
|
||||
SCH_EDIT_FRAME* fr = schFrame();
|
||||
|
||||
if( !fr )
|
||||
return arr.dump();
|
||||
|
||||
for( const presence::PEER& peer : presence::g_peers )
|
||||
{
|
||||
for( const KIID& id : peer.xsel )
|
||||
{
|
||||
SCH_SHEET_PATH path;
|
||||
SCH_ITEM* item = fr->Schematic().ResolveItem( id, &path, /*allowNull*/ true );
|
||||
|
||||
if( item && path.LastScreen() == fr->GetScreen() )
|
||||
arr.push_back( toUtf8( item->m_Uuid.AsString() ) );
|
||||
}
|
||||
}
|
||||
|
||||
return arr.dump();
|
||||
}
|
||||
|
||||
// Test helper: REALLY select the current sheet's first item through the selection
|
||||
// tool, then run the presence check (programmatic selects close no canvas event).
|
||||
std::string schCollabTestSelectFirst()
|
||||
|
|
@ -1812,6 +1888,48 @@ std::string schCollabTestSelectFirst()
|
|||
return toUtf8( target->m_Uuid.AsString() );
|
||||
}
|
||||
|
||||
// Test helper (0006): REALLY select the first SYMBOL on the current sheet —
|
||||
// the deterministic cross-app subject (TestSelectFirst may pick a wire, which
|
||||
// legitimately maps to nothing in pcbnew). Returns the uuid, "" without one.
|
||||
std::string schCollabTestSelectComponent()
|
||||
{
|
||||
SCH_EDIT_FRAME* fr = schFrame();
|
||||
|
||||
if( !fr )
|
||||
return "";
|
||||
|
||||
SCH_SCREEN* screen = currentScreen( fr );
|
||||
|
||||
if( !screen )
|
||||
return "";
|
||||
|
||||
SCH_ITEM* target = nullptr;
|
||||
|
||||
for( SCH_ITEM* item : screen->Items().OfType( SCH_SYMBOL_T ) )
|
||||
{
|
||||
// Power symbols (PWR_FLAG, GND, …) legitimately have no footprint —
|
||||
// they'd make the cross-app subject map to nothing by construction.
|
||||
if( static_cast<SCH_SYMBOL*>( item )->IsPower() )
|
||||
continue;
|
||||
|
||||
target = item;
|
||||
break;
|
||||
}
|
||||
|
||||
if( !target )
|
||||
return "";
|
||||
|
||||
fr->CallAfter( [fr, target]() {
|
||||
if( SCH_SELECTION_TOOL* st = fr->GetToolManager()->GetTool<SCH_SELECTION_TOOL>() )
|
||||
{
|
||||
st->AddItemToSel( target );
|
||||
schedulePresenceSelCheck();
|
||||
}
|
||||
} );
|
||||
|
||||
return toUtf8( target->m_Uuid.AsString() );
|
||||
}
|
||||
|
||||
// Test helper: clear the selection through the tool + run the presence check.
|
||||
bool schCollabTestClearSelection()
|
||||
{
|
||||
|
|
@ -1903,7 +2021,11 @@ EMSCRIPTEN_BINDINGS(eeschema) {
|
|||
function("kicadCollabTestDemoSet", &schCollabTestDemoSet);
|
||||
function("kicadCollabGetViewport", &schCollabGetViewport);
|
||||
function("kicadCollabGetSelection", &schCollabGetSelection);
|
||||
// Cross-app selection (0006).
|
||||
function("kicadCollabGetSelectionFull", &schCollabGetSelectionFull);
|
||||
function("kicadCollabTestGetCrossMapped", &schCollabTestGetCrossMapped);
|
||||
function("kicadCollabTestSelectFirst", &schCollabTestSelectFirst);
|
||||
function("kicadCollabTestSelectComponent", &schCollabTestSelectComponent);
|
||||
function("kicadCollabTestClearSelection", &schCollabTestClearSelection);
|
||||
#endif // !KICAD_MERGED_EMBIND
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue