Approved design for porting KiCad-wasm editors to collaborative editing via Yjs: - 0001 general (tool-agnostic) bridge contract: structured CRDT, unified ChangeSource + apply, the 3 serialization mechanisms, 4-commit phasing - 0002 pl_editor (first PoC): identity, snapshot-differ adapter, field mapping - 0003 eeschema, 0004 pcbnew: native-listener/COMMIT adapters Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.9 KiB
pcbnew (PCB editor) — bridge specifics
Differences from 0001-general-bridge-design.md. pcbnew implements the same unified bridge
contract (general §1) with the richest native hooks of the three. Two things dominate its
specifics: mandatory connectivity/ratsnest recompute and scale (10k–100k+ items).
Adapter — native (richest of the three) — and why
- Identity free:
BOARD_ITEM⊂EDA_ITEM→const KIID m_Uuid(include/eda_item.h:516), serialized as(uuid …)on every item byPCB_IO_KICAD_SEXPR(e.g. trackpcb_io_kicad_sexpr.cpp:2619, footprint:1170, zone:2647). Fast lookup viastd::unordered_map<KIID, BOARD_ITEM*> m_itemByIdCache(board.h:1458). No format change. - Granular change set:
BOARD_COMMIT::Push(pcbnew/board_commit.cpp:176–631) tags each entryCHT_ADD/REMOVE/MODIFYand collects bulk add/remove/changed vectors. - Native observer — hook without patching core:
BOARD_LISTENER(board.h:283–301) is the richest interface — singular and bulk callbacks, plus a combined one:
Registered viaOnBoardItemAdded/ItemsAdded · OnBoardItemRemoved/ItemsRemoved OnBoardItemChanged/ItemsChanged · OnBoardNetSettingsChanged OnBoardHighlightNetChanged · OnBoardRatsnestChanged OnBoardCompositeUpdate(board, added[], removed[], changed[]) // ← one event, all three setsboard->AddListener(bridge)(board.cpp:2964); fired throughInvokeListeners(...)(board.h:1424).BOARD_COMMIT::Push→OnItemsCompositeUpdate(board_commit.cpp:580→board.cpp:3001).OnBoardCompositeUpdateis the ideal single emit hook.
Model — typed containers (vs flat list / vs rtree)
8 typed containers, not one list (board.h:1447–1455): m_footprints, m_tracks, m_zones,
m_drawings, m_groups, m_generators, m_markers, m_points. The Yjs shape is still a
flat uuid-keyed Y.Array — type tells apply which container BOARD::Add routes to;
m_itemByIdCache resolves uuid→item in O(1).
ChangeSource (emit)
BOARD_LISTENER::OnBoardCompositeUpdate → emit per-item JSON for added/changed (uuid+type+
fields) and uuid-only for removed. Converter strategy: reflection for the bulk + native blob
for the long tail — pcbnew uniquely has both generic mechanisms (general §3):
- (1)
BOARD_ITEMis broadlyINSPECTABLE(board_item.cpp:427+) →PROPERTY_MANAGERserializes most fields write-once with field-level merge. - (2)
PCB_IO_KICAD_SEXPR::Format(const BOARD_ITEM*)(pcb_io_kicad_sexpr.h:351, per-typeformat(PCB_TRACK*/PAD*/ZONE*/…)) gives a free opaque blob for any type reflection misses.
So: reflection-decompose the hot types you want field-level merge on (track width/endpoints, position), blob everything else — no hand-written per-type serializers.
apply — go through BOARD_COMMIT; recompute is MANDATORY
BOARD_COMMIT commit( toolMgr );
// add: item = deserialize(...); commit.Add(item)
// modify: item = board->GetItem(uuid); commit.Modify(item); …edit…
// remove: commit.Remove(board->GetItem(uuid))
commit.Push( "collab" ); // updates connectivity + recomputes ratsnest
Push updates CONNECTIVITY_DATA per item and calls RecalculateRatsnest. Never use
direct BOARD::Add/Remove for remote ops unless you also manually
GetConnectivity()->Add/Update/Remove + RecalculateRatsnest() + OnRatsnestChanged() —
the commit path does it correctly, so use it.
Apply gotchas (enumerated)
- Connectivity + ratsnest are mandatory after any structural/net change — skipping corrupts net feedback. This is the single biggest difference from pl_editor.
- Zones are expensive — outline edits need a re-fill (not automatic).
- Footprints own child items (pads, fp text); their uuids live in
m_itemByIdCachetoo — a footprint add/remove cascades (handled byBOARD::Add,board.cpp:1239). - Vias/teardrops/DRC — drill changes ripple;
BOARD_COMMIT::Pushhandles within commit. - Groups — membership tracked by the commit.
- Net settings (
OnBoardNetSettingsChanged) are board-global, not per-item — model as a separate doc field, not an items entry.
Scale — the defining constraint
Real boards: 5k–20k items typical, 50k–500k for large designs. Implications:
- Snapshot-on-change is non-viable (per-commit serialization → MB; remote parse blocks UI). Granular deltas via the listener are essential here — never a whole-document approach.
- Initial sync is itself heavy: seeding a fresh
Y.Docfrom a 100k-item board must be a batched, own-phase operation (chunked transactions, possibly off the first paint), not a single emit. Flag as its own sub-task within the pcbnew commit. - Prefer opaque-blob items to keep per-item payloads small and avoid a huge field-mapping surface across pcbnew's many types.