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

78 lines
4.9 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 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:176–631`) tags each
entry `CHT_ADD/REMOVE/MODIFY` and 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:
```cpp
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::Push` → `OnItemsCompositeUpdate`
(`board_commit.cpp:580` → `board.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: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_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
```cpp
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: 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.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.