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>
This commit is contained in:
parent
7d49007473
commit
f94114efb8
4 changed files with 515 additions and 0 deletions
78
features/yjs-bridge/0004-pcbnew-bridge.md
Normal file
78
features/yjs-bridge/0004-pcbnew-bridge.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# 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.
|
||||
Loading…
Reference in a new issue