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>
78 lines
4.9 KiB
Markdown
78 lines
4.9 KiB
Markdown
# 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.
|