2026-06-03 13:21:24 +02:00
|
|
|
|
# pl_editor (drawing-sheet editor) — bridge specifics
|
|
|
|
|
|
|
|
|
|
|
|
Differences from `0001-general-bridge-design.md`. pl_editor implements the **same unified
|
|
|
|
|
|
bridge contract** as the other two (per-item `ChangeSource` + per-item `apply`); it is the
|
|
|
|
|
|
**first PoC**. It has none of the native machinery, so its `ChangeSource` is the one adapter we
|
|
|
|
|
|
write from scratch — an **in-bridge snapshot-differ** — while its edit/undo core stays
|
|
|
|
|
|
untouched (low upstream divergence; Route 1, decided 2026-06-03).
|
|
|
|
|
|
|
|
|
|
|
|
## What pl_editor lacks natively (why its adapter is custom)
|
|
|
|
|
|
- `DS_DATA_ITEM` does **not** derive from `EDA_ITEM` (`include/drawing_sheet/ds_data_item.h:95`)
|
|
|
|
|
|
→ no `KIID`, and the `COMMIT` base operates on `EDA_ITEM*` so it can't take data items.
|
|
|
|
|
|
- **No COMMIT** anywhere in `pagelayout_editor/` (0 uses); undo is a whole-model S-expr snapshot
|
|
|
|
|
|
(`SaveCopyInUndoList` → `DS_PROXY_UNDO_ITEM`) wired at 13 sites across 8 files.
|
|
|
|
|
|
- **No listener/observer** of any kind.
|
|
|
|
|
|
- Single change chokepoint: `PL_EDITOR_FRAME::OnModify()` (`pl_editor_frame.cpp:379`).
|
|
|
|
|
|
|
|
|
|
|
|
We do **not** rewrite that core. Instead the adapter derives per-item events by diffing, and
|
|
|
|
|
|
applies per-item by uuid — producing the exact same observable behavior as `SCH_COMMIT`/
|
|
|
|
|
|
`BOARD_COMMIT`, encapsulated entirely in the bridge.
|
|
|
|
|
|
|
|
|
|
|
|
## Model
|
|
|
|
|
|
- Singleton `DS_DATA_MODEL::GetTheInstance()`; `std::vector<DS_DATA_ITEM*> m_list`
|
|
|
|
|
|
(`include/drawing_sheet/ds_data_model.h`). Flat, single-document — no sheets/screens, no
|
|
|
|
|
|
connectivity/ERC (a big simplification vs eeschema/pcbnew).
|
|
|
|
|
|
- Item types: `DS_TEXT`, `DS_SEGMENT`, `DS_RECT`, `DS_POLYPOLYGON`, `DS_BITMAP`.
|
|
|
|
|
|
- `DS_DRAW_ITEM_*` render proxies are rebuilt by `DS_DATA_ITEM::SyncDrawItems(view)`; ephemeral.
|
|
|
|
|
|
|
|
|
|
|
|
## Identity — add the SAME `KIID`/`(uuid …)` as eeschema/pcbnew
|
|
|
|
|
|
Use the identical type + token + writer (general §2), not a bespoke id:
|
|
|
|
|
|
- Add `KIID m_Uuid;` to `DS_DATA_ITEM` (`include/kiid.h`). Default-constructed → **random**, so
|
|
|
|
|
|
a new or uuid-less item is auto-backfilled in memory for free.
|
|
|
|
|
|
- Serialize with the shared helper `KICAD_FORMAT::FormatUuid( m_out, item->m_Uuid )`
|
|
|
|
|
|
(`include/io/kicad/kicad_io_utils.h:55`) in `ds_data_model_io.cpp`, exactly as sch/pcb do.
|
|
|
|
|
|
|
|
|
|
|
|
### Save + load + backfill
|
|
|
|
|
|
- **Format:** add `uuid` to `common/drawing_sheet/drawing_sheet.keywords` (absent today). The
|
|
|
|
|
|
wks lexer is **strict** (unknown token → `default:` → `Expecting()`/throws), so the token must
|
|
|
|
|
|
be declared + handled — no silent passthrough.
|
|
|
|
|
|
- **Load:** `case T_uuid: item->m_Uuid = KIID( FromUTF8() ); NeedRIGHT(); break;` in each item
|
|
|
|
|
|
parser (`drawing_sheet_parser.cpp`). If the token is absent, the parser leaves the default
|
|
|
|
|
|
random `m_Uuid` in place → **automatic backfill**.
|
|
|
|
|
|
- **Save:** always write `(uuid …)` per item → the first save of a legacy/upstream uuid-less
|
|
|
|
|
|
`.kicad_wks` **upgrades the file** in place.
|
|
|
|
|
|
- **Cross-client consistency:** files with uuids (our output, and any file after one save) →
|
|
|
|
|
|
all clients agree. A never-saved uuid-less legacy file gets *random* backfill
|
|
|
|
|
|
(`KIID(invalid)` → random, confirmed in `common/kiid.cpp` — not a deterministic hash), so two
|
|
|
|
|
|
cold opens disagree; handled by the **seed-once rule** (general §2). Optional deterministic
|
|
|
|
|
|
content-seed backfill (boost `name_generator` over type+name+pos+index) is deferred hardening.
|
|
|
|
|
|
- **Tradeoff:** files now carry `(uuid …)`; stock desktop KiCad's strict parser rejects the
|
|
|
|
|
|
unknown token → fork-only files. Backfill makes the upgrade graceful.
|
|
|
|
|
|
|
|
|
|
|
|
## `ChangeSource` adapter — in-bridge snapshot-differ (emits per-item deltas)
|
|
|
|
|
|
The bridge holds the **last emitted snapshot** (a `uuid → fields` map). On `OnModify()`:
|
|
|
|
|
|
1. Serialize current `m_list` to a `uuid → fields` map — **all 5 types, none invisible**
|
|
|
|
|
|
(rationale below): text/segment/rect as scalars; poly/bitmap as an opaque S-expr blob.
|
|
|
|
|
|
2. **Diff vs the cached map by uuid:** present-only-in-new → `added`; only-in-old → `removed`;
|
|
|
|
|
|
in-both-with-changed-fields → `changed`.
|
|
|
|
|
|
3. Emit the per-item delta JSON via `EM_ASM window.kicadCollab.onDelta(json)`; update the cache.
|
|
|
|
|
|
|
|
|
|
|
|
This yields the same `added/removed/changed` per-item events the native listeners produce. The
|
|
|
|
|
|
diff is O(items) — negligible for drawing sheets. *(We could instead call the differ directly
|
|
|
|
|
|
from `OnModify`; optionally wrap it as a one-method `DS_DATA_MODEL` listener purely for
|
|
|
|
|
|
registration symmetry with sch/pcb — nice-to-have, not required.)*
|
|
|
|
|
|
|
|
|
|
|
|
## `apply(delta)` — per-item mutation by uuid
|
|
|
|
|
|
For each op in the incoming delta (inside the `s_applyingRemote` guard, general §5):
|
|
|
|
|
|
- **changed:** find `DS_DATA_ITEM` by `m_Uuid` in `m_list`; set the changed fields.
|
|
|
|
|
|
- **added:** create the item (with the given uuid), `DS_DATA_MODEL::Append`.
|
|
|
|
|
|
- **removed:** find by uuid, `Remove` + delete.
|
|
|
|
|
|
Then `SyncDrawItems` on touched items + canvas refresh, and **re-select by uuid** so a remote
|
|
|
|
|
|
apply doesn't drop local selection (a bonus the uuid unlocks). Optionally
|
|
|
|
|
|
`SaveCopyInUndoList()` once after a remote batch so local undo has a coherent checkpoint. **No
|
|
|
|
|
|
connectivity/ERC** — none exists for drawing sheets.
|
|
|
|
|
|
|
|
|
|
|
|
## Text-insert path (working UI flow targeted first)
|
|
|
|
|
|
`PL_DRAWING_TOOLS::PlaceItem` (`tools/pl_drawing_tools.cpp:72`) → `AddDrawingSheetItem(DS_TEXT)`
|
|
|
|
|
|
(`pl_editor_frame.cpp:823`) → `new DS_DATA_ITEM_TEXT("Text")` (fresh `KIID`) → `Append` →
|
|
|
|
|
|
`SyncDrawItems` → `SaveCopyInUndoList()` → `OnModify()` ← differ runs here → emits an `added`.
|
|
|
|
|
|
|
|
|
|
|
|
## Field mapping — all 5 types in one pass, at two fidelities
|
|
|
|
|
|
Cover every type so none is invisible to collaboration (see rationale). pl_editor has no
|
|
|
|
|
|
reflection (`DS_*` is not `EDA_ITEM`), so scalars are hand-mapped (general §3 mechanism 3) and
|
|
|
|
|
|
the awkward two reuse the existing per-item serializer (mechanism 2):
|
|
|
|
|
|
|
|
|
|
|
|
Shared: `id`(m_Uuid) · `type` · `name`(m_Name) · `x,y`(m_Pos.m_Pos, mm) · `anchor`(m_Pos.m_Anchor)
|
|
|
|
|
|
- **text** (scalars): `text`(m_TextBase) · `orient`(°) · `hjustify`/`vjustify` · `italic` ·
|
|
|
|
|
|
`bold` · `sizeX,sizeY`(m_TextSize, mm) — field-level merge.
|
|
|
|
|
|
- **segment/rect** (scalars): `ex,ey`(m_End.m_Pos) · `eanchor` · `linewidth`(m_LineWidth).
|
|
|
|
|
|
- **poly/bitmap** (opaque blob): `sexpr` field from `DS_DATA_MODEL_IO::Format(model, item)` /
|
|
|
|
|
|
`SaveInString({item}, &str)` — *no* nested corner-list / base64 field mapper to write;
|
|
|
|
|
|
item-level merge. Refine to scalars later only if field-level merge is ever wanted.
|
|
|
|
|
|
|
|
|
|
|
|
**Why all 5, not just text/line/rect:** the adapter is a whole-model differ, so a type absent
|
|
|
|
|
|
from the snapshot is **invisible** — a user adding a polygon or logo bitmap simply wouldn't
|
|
|
|
|
|
sync. Including poly/bitmap as blobs closes that correctness gap at near-zero cost (the
|
|
|
|
|
|
serializer already exists). **Bitmap is safe** despite its large base64 payload: the differ
|
|
|
|
|
|
emits a field only when its value changes (string compare), so a static image is cached once
|
|
|
|
|
|
and never re-emitted — just keep it out of any per-keystroke re-encode path.
|
|
|
|
|
|
|
|
|
|
|
|
On **apply**, a blob item is reconstructed by parsing its `sexpr` back into a `DS_DATA_ITEM`
|
|
|
|
|
|
(wrap as a minimal model string → reuse `SetPageLayout`/the parser; a tiny parse-one helper).
|
|
|
|
|
|
|
|
|
|
|
|
## Build steps (maps to phasing commits 1–2 in general §8)
|
|
|
|
|
|
**Commit 1 — interface/identity:** `KIID m_Uuid` on `DS_DATA_ITEM` + `(uuid)`
|
|
|
|
|
|
keyword/parse(backfill)/save via `FormatUuid`. Build once; verify a `.kicad_wks` round-trips
|
|
|
|
|
|
uuids and a legacy file is backfilled on save.
|
2026-06-03 14:25:33 +02:00
|
|
|
|
**Commit 2 — bridge + converters (all 5 types) + tests:** *(done 2026-06-03)*
|
|
|
|
|
|
|
|
|
|
|
|
> **Placement update (as built):** the bridge does **not** live in
|
|
|
|
|
|
> `common/drawing_sheet/ds_collab_bridge.{h,cpp}` (kicad fork). To honor CLAUDE.md
|
|
|
|
|
|
> ("fix in the wasm layer / keep the fork close to upstream") the entire
|
|
|
|
|
|
> differ/apply/emit was implemented in the **wasm layer**,
|
|
|
|
|
|
> `wasm/bindings/pl_editor_embind.cpp` (root repo), using only public
|
|
|
|
|
|
> `DS_DATA_MODEL`/`DS_DATA_ITEM` API. **Fork footprint = one file, +15 lines:** an
|
|
|
|
|
|
> `extern "C" void kicadCollabOnModify()` call wired into `PL_EDITOR_FRAME::OnModify()`.
|
|
|
|
|
|
> Same architecture as below; only the file location differs.
|
|
|
|
|
|
|
|
|
|
|
|
1. Bridge (`wasm/bindings/pl_editor_embind.cpp`): the snapshot-differ `ChangeSource`
|
|
|
|
|
|
(cache + diff, emit on `OnModify` via `EM_ASM`), `kicadCollabApply` per-item-by-uuid
|
|
|
|
|
|
(scalars by field; poly/bitmap via `SetPageLayout`-append blob), `s_applyingRemote`
|
|
|
|
|
|
guard, plus `kicadCollabSnapshot()` (seed/baseline) and a `kicadCollabTestAddText()`
|
|
|
|
|
|
PoC local-edit hook.
|
|
|
|
|
|
2. Web: `yjs` + `BroadcastChannel` provider; the generic reconciler (general §4) under
|
|
|
|
|
|
`web/apps/frontend/src/wasm/collab/`. CRDT = uuid-keyed `Y.Map` of per-item `Y.Map`
|
|
|
|
|
|
(refinement of "Y.Array<Y.Map>" — better for id-stable items). `WasmTool` wiring +
|
|
|
|
|
|
Zod guard deferred.
|
|
|
|
|
|
3. Verified by `tests/kicad/pl_editor-collab.spec.ts`: single-page C++ contract
|
|
|
|
|
|
(snapshot / apply changed+removed+added / echo-suppression) + two-tab BroadcastChannel
|
|
|
|
|
|
A↔B text propagation through the real reconciler. Selection re-acquire-by-uuid and
|
|
|
|
|
|
screenshots deferred.
|
2026-06-03 13:21:24 +02:00
|
|
|
|
|
|
|
|
|
|
## Risks specific to pl_editor
|
|
|
|
|
|
- Carries the **format-change divergence** (general §9) — the only tool that does.
|
|
|
|
|
|
- Its `ChangeSource` is a **derived** differ (not native tracking). Correct and cheap here, but
|
|
|
|
|
|
the differ pattern is pl_editor's alone — eeschema/pcbnew use native events.
|
|
|
|
|
|
- Never-saved legacy-file cold-open identity race — mitigated by seed-once; deterministic
|
|
|
|
|
|
backfill deferred.
|