pcbjam/features/yjs-bridge/0004-pcbnew-bridge.md
Gergő Törcsvári f94114efb8
docs(yjs-bridge): add collaborative Yjs bridge design
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>
2026-06-05 13:56:41 +02:00

4.9 KiB
Raw Blame History

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 (10k100k+ items).

Adapter — native (richest of the three) — and why

  • Identity free: BOARD_ITEMEDA_ITEMconst KIID m_Uuid (include/eda_item.h:516), serialized as (uuid …) on every item by PCB_IO_KICAD_SEXPR (e.g. track pcb_io_kicad_sexpr.cpp:2619, footprint :1170, zone :2647). Fast lookup via std::unordered_map<KIID, BOARD_ITEM*> m_itemByIdCache (board.h:1458). No format change.
  • Granular change set: BOARD_COMMIT::Push (pcbnew/board_commit.cpp:176631) tags each entry CHT_ADD/REMOVE/MODIFY and collects bulk add/remove/changed vectors.
  • Native observer — hook without patching core: BOARD_LISTENER (board.h:283301) is the richest interface — singular and bulk callbacks, plus a combined one:
    OnBoardItemAdded/­ItemsAdded · OnBoardItemRemoved/­ItemsRemoved
    OnBoardItemChanged/­ItemsChanged · OnBoardNetSettingsChanged
    OnBoardHighlightNetChanged · OnBoardRatsnestChanged
    OnBoardCompositeUpdate(board, added[], removed[], changed[])   // ← one event, all three sets
    
    Registered via board->AddListener(bridge) (board.cpp:2964); fired through InvokeListeners(...) (board.h:1424). BOARD_COMMIT::PushOnItemsCompositeUpdate (board_commit.cpp:580board.cpp:3001). OnBoardCompositeUpdate is the ideal single emit hook.

Model — typed containers (vs flat list / vs rtree)

8 typed containers, not one list (board.h:14471455): 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.Arraytype 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_ITEM is broadly INSPECTABLE (board_item.cpp:427+) → PROPERTY_MANAGER serializes most fields write-once with field-level merge.
  • (2) PCB_IO_KICAD_SEXPR::Format(const BOARD_ITEM*) (pcb_io_kicad_sexpr.h:351, per-type format(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_itemByIdCache too — a footprint add/remove cascades (handled by BOARD::Add, board.cpp:1239).
  • Vias/teardrops/DRC — drill changes ripple; BOARD_COMMIT::Push handles 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: 5k20k items typical, 50k500k 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.Doc from 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.