pcbjam/features/yjs-bridge/0003-eeschema-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

5.2 KiB
Raw Blame History

eeschema (schematic editor) — bridge specifics

Differences from 0001-general-bridge-design.md. eeschema implements the same unified bridge contract (general §1); its ChangeSource/apply adapter is a thin re-use of native machinery, so most of pl_editor's hard work (0002) disappears here. Two genuinely new things: multi-sheet hierarchy and symbol instances.

Adapter — native (no derived differ, no format change) — and why

eeschema has the full machinery natively:

  • Identity free: SCH_ITEMEDA_ITEMconst KIID m_Uuid (include/eda_item.h:516), serialized as (uuid …) on every item by SCH_IO_KICAD_SEXPR (eeschema/sch_io/kicad_sexpr/sch_io_kicad_sexpr.cpp:745,1047,1189,1322,…; parsed in …_parser.cpp:2777,3476,…). No format change (contrast 0002).
  • Granular change set: SCH_COMMIT (eeschema/sch_commit.h), Push() loop (eeschema/sch_commit.cpp:228397) tags each entry CHT_ADD / CHT_REMOVE / CHT_MODIFY.
  • Native observer — hook without patching core: SCHEMATIC_LISTENER (eeschema/schematic.h:6071):
    virtual void OnSchItemsAdded   ( SCHEMATIC&, std::vector<SCH_ITEM*>& );
    virtual void OnSchItemsRemoved ( SCHEMATIC&, std::vector<SCH_ITEM*>& );
    virtual void OnSchItemsChanged ( SCHEMATIC&, std::vector<SCH_ITEM*>& );
    virtual void OnSchSheetChanged ( SCHEMATIC& );
    
    Register via schematic->AddListener(bridge) (schematic.h:359). SCH_COMMIT::Push fires these in bulk (sch_commit.cpp:399417). → emit is a listener subclass; near-zero fork divergence.

So: no derived differ and no format patch here. The ChangeSource is the listener; apply goes through SCH_COMMIT.

Model — multi-sheet (the main structural difference)

  • SCHEMATIC (eeschema/schematic.h:87) → virtual m_rootSheet, m_topLevelSheets, current m_currentSheet. Each SCH_SHEET owns one SCH_SCREEN whose items live in an EE_RTREE (eeschema/sch_screen.h:98117).
  • Yjs shape: because m_Uuid is globally unique, a flat uuid-keyed Y.Array still works, but each item's Y.Map must carry a screen/sheetPath field so apply knows which SCH_SCREEN to target. (Alternative: a Y.Map of screens, each a Y.Array of items — more structure, mirrors the model; decide during the eeschema commit. Flat+scope-field is simpler to start.)
  • OnSchSheetChanged / hierarchy refresh must propagate as its own doc field/event.

Symbol instances (the other new wrinkle)

A SCH_SYMBOL placed in a shared sheet appears on multiple sheet instances; per-instance data (reference, unit) lives in m_instanceReferences (SCH_SYMBOL_INSTANCE). Editing the shared symbol affects all instances; a remote change to reference/unit must update the right instance record, not just the symbol. Treat instance data as nested fields keyed by sheet-path within the symbol's item.

ChangeSource (emit)

SCHEMATIC_LISTENER subclass → on OnSchItemsAdded/Removed/Changed, emit per-item JSON. Converter strategy: reflection-first (general §3 mechanism 1) — SCH_ITEM : EDA_ITEM, so use PROPERTY_MANAGER to serialize registered fields write-once; hand-map (mechanism 3) the gaps where properties aren't registered. (Blobs are not an option here — see serialization note.) Include the screen/sheetPath scope on every item.

apply — go through SCH_COMMIT

SCH_COMMIT commit( toolMgr );
// add:    create SCH_ITEM, commit.Add(item, screen)
// modify: item = schematic->ResolveItem(uuid); commit.Modify(item, screen); …edit fields…
// remove: commit.Remove(item, screen)
commit.Push( "collab" );   // triggers the recompute below

Push (sch_commit.cpp) runs the required side-effects:

  • Connectivity/ERC/netlist: frame->RecalculateConnections(...) when any connectable item changed (HasConnectivityChanges); local vs global cleanup chosen automatically.
  • Hierarchy: schematic->RefreshHierarchy() + nav update when a SCH_SHEET changed.

Apply gotchas (enumerated)

  • Connectivity is bidirectional — adding/removing a wire re-nets connected pins; never skip RecalculateConnections.
  • Symbol instances — update the per-sheet instance record, not only the shared symbol.
  • Groups — keep parent/child links in sync (group->AddItem/RemoveItem).
  • SCH_FIELD remove is special — it hides, not deletes (sch_commit.cpp:308).
  • ERC markers are uuid-tied; deletes orphan them (respect exclusions).
  • Selection is transient UI state — never propagate.

Serialization note (differs from pcbnew)

eeschema's I/O plugin is document-level, not per-item — there is no clean public Format(single SCH_ITEM), so the opaque-S-expr-blob path (general §3 mechanism 2) is not available here. This is exactly why the converter is reflection-first + hand-mapped gaps: fields via PROPERTY_MANAGER (or accessors), and on apply build complex items via Clone() + field setters rather than re-parsing a blob.

Scale

~5005000 items typical (10k+ for large hierarchical designs). Granular deltas (listener) are strongly preferred; whole-document serialization is expensive (hierarchy + uuid resolve + connectivity). A pl_editor-style derived differ would be wrong at this scale — use the listener.