Kad — Visual editor for KVG projects
- Area:
products/horizontal/kad - Kind: Vector + CAD + 3D + interactive-simulation editor on the KVG format
- Category: Visio + draw.io + SketchUp + AutoCAD + Blender, the Koder way
- Stack: DartFlutter app over
koder_kit; Go backend (kadd); KVG engine (`engineslang/kvg`) - Binary:
kadd(backend) · port7975· store: kdb - RFCs:
rfcs/kvg-RFC-001(Interact profile, ratified 2026-06-07),rfcs/kvg-RFC-002(Kad product + shared layers)
Role in the stack
Kad is Koder's visual editor for KVG (Koder Vector Graphics) projects: a single canvas where a diagram, a CAD drawing, a 3D model, and an interactive simulation are all the same document. Objects are selectable/editable vector primitives backed by themed stencil libraries (BPMN, UML, ER, flowcharts, floor plans, furniture, circuits, PCB/IC, signage, characters, devices, …), and the signature transform turns a 2D floor plan into a walkable, interactive 3D house — flip a switch to light a lamp, drag the sun to re-light the walls.
It is built deliberately on shared layers (kvg-RFC-002 §3, reuse-first ≥3 consumers) so Kad, Dok (textual KVG editing), and Craft (migration) all stand on the same code, and so the 2D→3D transform and the Interact runtime are identical from the app, the CLI, or a server batch.
Architecture — three shared layers (kvg-RFC-002)
- KVG editing-core —
engines/sdk/koder_kit/lib/src/kvg/editor/(Dart, headless,no Flutter): mutable observable document model, selection + hit-test (z-order) + affine transforms, undo/redo command stack, canonical-KSS round-trip serializer, property/
@paramsurfacing. The unit every Kad surface (and Dok) stands on. - Stencil registry client —
engines/sdk/koder_kit/lib/src/kvg/stencils/(Dart):pack manifest parsing, catalog + profile filtering, and the §8 insertion contract (copy the transitive
kdefclosure namespaced<domain>__<name>, place ause of=#…instance, dedupe). Spec:specs/kvg/stencils.kmd; registry:registries/kvg-stencils.md. First-party packs live inengines/lang/kvg/stencils/(geometry-2d,flowchart,floorplan). - 2D→3D pipeline —
engines/lang/kvg/kgen/floorplan.go(Go):emit_wall/emit_floor/emit_openingkgen pragmas extrude a 2D plan into Solid geometry, identical from KadDokCLI/batch.
The Interact runtime (kvg-RFC-001 §3)
The interactive half — scene state, @param knobs, and on <event> -> <action> rules — is implemented end-to-end as logic (renderer/camera integration is the remaining slice, coordinated with the Solid renderer):
- Parser (both languages):
state{}/@param(...)/on…->tokens + AST in theGo engine (
engines/lang/kvg/parser) and the Dart parser (koder_kit), producing identicalstatevarparamrulenodes; validators for the closed event/action vocabularies + state-var types. - Runtime (both languages):
engines/lang/kvg/interact(Go) andkoder_kit/lib/src/kvg/interact(Dart) — a deterministicScenewith event dispatch,toggle/setactions, ternary + relational expressions, node-property overrides (NodeProps), and anon changecascade (value-equality gate + depth guard). The "flip switch → toggle lamp → re-light" mechanic runs headless in both.
Backend (kadd, port 7975)
products/horizontal/kad/backend/ (Go) — multi-tenant document store + CRUD. Tenant is resolved by auth, never client-supplied; cross-tenant access → 404 not 403 (multi-tenant-by-default.kmd; contract T3/T6); the store sits behind a DocStore interface so the kdb backing (stack-RFC-001) drops in without touching the API. Large Solid meshes/assets target the object-storage plane (stack-RFC-006). Documents and user stencils share one generic tenant-scoped store (Store[T], KAD-012 — the cross-tenant→404 isolation written once; a new collection is a new codec, not a new store), in both the in-memory and kdb backends.
Product structure (RFC-006)
products/horizontal/kad/
├── backend/ # Go API (kadd): documents, stencil sync, collab, multi-tenant store
├── app/ # Flutter surfaces, thin over a shared package (RFC-006 §L4):
│ ├── core/ # shared `koder_kad`: canvas + editing-core binding, screens, shell
│ ├── desktop/ # primary edit surface (Linux/macOS/Windows)
│ ├── web/ # Flutter Web at kad.koder.dev (parity with desktop)
│ └── mobile/ # view + light-edit (Android/iOS)
├── landing/ # kad.koder.dev product landing
└── backlog/The app surfaces never re-implement the editing surface: app/core (package koder_kad) binds the canvas to the headless editing-core (KAD-001) once and the per-target shells embed it (KAD-009 — reuse-first). The canvas renders via the SDK's KvgPainter and drives KvgEditor; surfaces reach the editing-core through the public package:koder_kit/kvg.dart facet.
Primary couplings
| Consumer / dependency | Relationship |
|---|---|
engines/sdk/koder_kit |
Editing-core + stencil client + Dart KVG parser/runtime (the shared layers) |
engines/lang/kvg |
The KVG format: parserrendererkgen, the floorplan-extrude pipeline, the Interact engine, first-party stencil packs |
products/horizontal/dok |
Co-consumer of the editing-core + stencil client (textual KVG editing) |
products/dev/hub |
Stencil pack distribution (Hub bundles) |
infra/data/kdb |
Document/state store (stack-RFC-001) |
services/foundation/id |
Auth — tenant resolution for the multi-tenant backend |
Status (2026-06-08)
Foundation + core shipped and verified off-laptop: editing-core (KAD-001), stencil client (KAD-002), first packs (KAD-003), floorplan→3D extrude (KAD-004), the Interact parser + runtime in Go and Dart (KAD-005 fatias 1–5c/5e), the backend multi-tenant store + CRUD isolation + generic Store[T] (KAD-006, KAD-012 fatia 1), and the Flutter app shell + live canvas + property panel (KAD-009 fatias 1–2a: app/core package — render + pan/zoom + select + drag-move + KoderApp shell, plus a property panel that edits the selected node's geometry/style through the editing-core's new setProperty write path — geometry routes to the positional arg slot the renderer reads; flutter analyze/test green on dev-linux-dek). The Dart renderer + bounds now also instantiate use of=#kdef (KAD-009 fatia 2b-i — render/instance.dart mirrors the engine's paintUse/resolveInstance, shared by painter + geometry), and the painter is now transform-aware (KAD-009 fatia 2b-ii — applies each node's transform=[a b c d e f], matching bounds.worldTransformOf; Dart↔Go parity), so a placed stencil draws, measures, selects and positions. On that foundation the stencil palette is now wired (KAD-009 fatia 2b — KadStencilPalette over an injected StencilCatalog: domain-grouped Draggable tiles + tap-to-insert; KadCanvas is a DragTarget; controller.insertStencilAt centers the dropped use at the cursor; a bootstrap catalog embeds the first-party Geometry 2D pack via LoadedPack.fromSources; 2222 `appcore tests green on dev-linux-dek). The **web shell** is now wired (KAD-009
fatia 2c-i — app/web, a thin usePathUrlStrategy()+bootstrapKad()+KadApp target for
kad.koder.dev; flutter build web green on dev-linux-dek, proving the shared package
assembles into a real web app). The **desktop shell** is now wired too (KAD-009 fatia
2c-ii — app/desktop, Flutter Linux+macOS+Windows over the same koder_kad package;
parses the standard CLI flags via the shared koder_kit runStandardFlags helper
(cli.dart facet — reuse-first, kruze/kover/backup/kad shared) first — --version/
--help print + exit before any
window, --user-data-dir isolates state — per binaries-and-cli/standard-flags.kmd;
the binary is kad and the window title "Koder Kad"; flutter build linux --release
green + kad --version → kad 0.1.0 on dev-linux-dek), meeting the desktop+web
parity acceptance gate for Linux. The **mobile shell** is now wired too (KAD-009 fatia
2c-ii — app/mobile, Flutter Android+iOS over the same koder_kad package; a thin
bootstrapKad()+KadApp main, no CLI flags; applicationId/namespace dev.koder.kad
+ app label "Koder Kad"; analyze clean on dev-linux-dek, APK build-verify routed to
ci-runner-android) — so **all three primary surfaces (web + desktop + mobile)** now
stand over the one shared package. The editor also draws a **parametric room from
measurements** (the owner's "SALA DIRETORIA — 9 m × 7,53 m" acceptance case — KAD-009:
koder_kit editorroom.dart RoomSpec`buildRoom emits a g of floor + 4 axis-aligned wall rects carrying its dims; controller.insertRoomByMeasures + a "Room" dialog; the engine floorplan-extrude kgen is the 3D counterpart for the 2D/3D toggle). The Core painter now also renders text (anchored, upright via a counter-flip of the y-up frame; bounds.dart measureshit-tests it) and *polygon`polyline (from points=[[x,y],…] via the shared kvgPoints reader — Go extrudePoints parity; fill closes per SVG semantics, stroke closes for polygon / open for polyline; bounds.dart measures + point-in-polygon hit-tests them) — the foundation for arbitrary-angle walls + the floorplan-extrude footprint. The text path closed a slice-1 gap, so the room shows its name and metric cotas on the plan (buildRoom(dimensions:true) draws dimension lines + ticks + metric labels on the width/length edges — the owner's room shows SALA DIRETORIA · 9 m · 7.53 m, completing "draw the room by measurements"). A placed room is re-editable (parametric loop): double-tap re-opens the dialog pre-filled and KvgEditor.editRoom regenerates it from new measures preserving position (undoable; RoomSpec.fromRoomNode reads the carried units/dims). A layers panel (KadLayersPanel, left rail) lists the scene's drawable nodes with two-way selection sync (tap a row → selects on canvas; the canvas selection highlights its row). The @param knobs are now surfaced (KAD-009 — kvg-RFC-001 §3.4 "drag the sun"): surfaceParams joins each @param to its bound state{} var (type → switch/slider, value from the live scene or the authored default), KadParamPanel (right rail, below the property panel, self-hiding) renders the typed controls, and controller.setParam writes the live KvgScene + cascades on change rules — live-preview state only (not undoable, never serialized), since the Core 2D canvas ignores Interact (§3.5) and the visible re-render is the Live view's job. That Live view now exists (KAD-009 — the view toggle, slice 1): an Edit ↔ Live SegmentedButton flips controller.viewMode; in Live mode KadScenePainter renders liveDocument() — a non-destructive KvgNode/ KvgDocument.clone() with applyScene materialising the scene overrides — so a @param knob (or any set #node.prop rule) visibly re-styles the canvas while the authored document stays pristine; the canvas suppresses editing gestures in Live (camera-only preview). The preview is now walkable: a Live tap routes through controller.liveTapAt — hit-testing the node and bubbling scene.dispatch('tap', id) up its id-bearing ancestors so on tap #switch -> toggle rules fire (+ the on change cascade re-styling the overlay). The drag gizmo is wired too ("drag the sun"): KvgScene.eventTargets tells the host which nodes are draggable, and controller.liveDragStart/liveDragTo map a Live drag to spherical (azimuth,
elevation) (the standard sun-path polar projection — the host-resolved pointer.spherical of §3.3) feeding scene.drag + cascade; the grab hit-tests at the pointer-down point (onPanDown), not the slop-delayed pan-start. The 2D Interact event grammar (tap + drag) is now complete. The 3D Spatial render is underway, sliced: (3a) the pure geometry (koder_kit kvg/solid/extrude.dart — extrudePrism turns a 2D footprint into a prism of Faces + isoProject/viewDepth, Go solid.go parity) + (3b) the painter rasterization are DONE — an extrude node now draws as a flat-shaded isometric prism on the canvas (KvgPainter case 'extrude': footprint from points=/a regular sides-gon → extrudePrism → faces drawn back-to-front by viewDepth, back-faces culled, each flat-shaded by normal·light; bounds.dart measures + hit-tests the projected silhouette). (3c-i) the camera+light seam is DONE too — KvgPainter(cameraRx, cameraRy) drives the turntable and sceneLight(doc) re-lights the prism off the scene's directional/light node (closing the loop with the @param "drag the sun" — its on change → set #sun.azimuth is materialised by applyScene). (3c-ii) the interactive turntable is DONE too — in Live mode an empty-space drag orbits the Spatial camera (controller.orbitCamera → KadScenePainter passes cameraRx/cameraRy to KvgPainter), so an extrude prism is rotatable on the canvas and re-lights off the @param sun. An extrude can also reuse a 2D shape's outline as its footprint (extrude of=#rect — extrudeFootprintkvgOutlineOf in `kvgsolid/extrude.dart, shared by the
painter + bounds; rect/circle/ellipse/polygon`). The whole 2D/3D view toggle is complete (Edit↔Live 2D overlay + tap/drag interactivity + the rotatable scene-lit 3D Spatial prism). A parametric room drawn by measures (RoomSpec/buildRoom) now also has its Spatial (3D) form: roomSolid extrudes it to a floor box + four extrude walls (front wall framed at the window sill), and the Live toggle lifts every room to its solid (liftRoomsToSolid). The floorplan emit (emit_wallemit_flooremit_opening) is now ported to the Dart kgen (1:1 with engines/lang/kvg/kgen/floorplan.go), so a floorplan extrudes identically on the Go-wasm web path and native Flutter; the painter renders box (Go boxGeom parity). CRDT collaboration (KAD-007) has its client op-model half in koder_kit/lib/src/kvg/collab/ (slice 2): KadCrdtState lowers each EditCommand to KadOp deltas (commandToOps) and materialises remote deltas (applyOps) with HLC-guarded LWW + add-wins membership + string fractional indexing — order-independent, idempotent, convergent. The authoritative merge / anti-entropy / persistence is server-side over the Rust koder-crdt crate (reuse- first per kvg-RFC-002 §5), NOT a second Dart merge engine. Slice 3a is done: infra/data/crdt/crates/koder-crdt-doc (DocCrdt) is the server-side graph-document CRDT — node membership over OrSet, typeparentorderattrsargs over LwwRegister, applymergematerialize — generic enough that Kad and Board both reuse it. Slice 3b is done too: koder-crdt-doc/service.rs CollabService<P: PersistStore> is the transport-agnostic service core — authoritative DocCrdt per WorkspaceId (multi-tenant, auth-derived), apply (apply+persist+fan-out deltas), snapshot (the op stream a late joiner applies, stamped below future writes), merge_remote (server-to-server), behind a PersistStore seam a kdb impl drops into. Slice 4 (client core) is done: koder_kit/collab gained a CollabTransport seam (duplex CollabMessage stream: opssnapshotjoin) + a LoopbackHub faithful to CollabService (per-workspace authoritative state, fan-out, snapshot-on-join) and KadCollabClient (drives a KadCrdtState over a transport — broadcast local commands via the new UndoStack.onPush chokepoint, apply remote deltas); KadEditorController.enableCollab(transport) wires it to the live editor so a peer's edit shows immediately. Slice 5 (presence) is done: cursors + selection ride a separate ephemeral channel (collab/presence.dart PresenceState/PresenceTracker — LWW-by-time + TTL, never in the document CRDT per §5), fanned out by the hub without touching the authoritative state; KadScenePainter draws each peer's selection box + cursor in a deterministic per-actor colour. KAD-007 is acceptance-complete (slice 6): the e2e test drives the real KadEditorControllers over the LoopbackHub end to end and proves the ticket's three clauses — two clients converge, concurrent same-node moves don't corrupt the graph (HLC-LWW single winner), a late joiner syncs via the join snapshot. The remaining collab work is deployment plumbing (KAD-013): slice 1 (the WebSocketCollabTransport Dart client adapter) is done — the production CollabTransport over web_socket_channel, auto-reconnect + outbox + last-join-replay-on-reconnect for transparent resync, tested against a real in-process WS server; slice 2a (the Rust wire codec koder-crdt-doc/wire.rs decoding the Dart CollabMessage/KadOp JSON into Ops — value-type-agnostic, storing tagged values verbatim for lossless round-trips — + the session.rs router handle() over CollabService, the Rust counterpart of the Dart LoopbackHub) is done, as is slice 2b-i (hub.rs CollabHub — the per-workspace connection registry + fan-out dispatch over session::handle, pure behind an OutboundSink trait so it's unit-tested without a socket), and slice 2b-ii-α (the koder-collabd tokio daemon — a new lib+bin crate in infra/data/crdt/crates/ wrapping CollabHub over tokio-tungstenite: ChannelSink implements OutboundSink over a per-conn unbounded mpsc drained by a writer task, the hub sits behind Arc<tokio::Mutex>, resolve_workspace derives the tenant from the ?workspace= query at connect — the auth seam — and a no-workspace connection is rejected; verified by a real tokio-tungstenite client roundtrip, 3/3, fan-out + late-joiner snapshot + tenant isolation), and slice 2b-ii-β-1 (the faithful DocCrdt state serialization — to_bytes/from_bytes dumping the full CRDT state, OR-set tags + every register's HLC, via a serde DTO that flattens the tuple-keyed attrs/args maps; needed a hand-written LwwRegister serde because a cleared attr value: Some(None) would otherwise JSON-collapse to null = never-set and silently un-clear on round-trip; persisting the true CRDT state — not a lossy materialize snapshot — is what makes reload(d).merge(other) == d.merge(other), so a restart never perturbs convergence; 3030, no regression on koder-crdt 7373), and slice 2b-ii-β-2 (the async kdb PersistStore — koder-collabd/src/kdb_persist.rs KdbPersistStore<KV: RawKv> persists the faithful β-1 blob per workspace so a document survives a daemon restart; bridges the sync PersistStore/async RawKv mismatch with a sync in-memory mirror backing load/save + a coalescing write-behind flush task that puts off-lock, plus warm-on-connect — serve/handle_conn generalised over P: PersistStore + a Warm hook that recovers a workspace's blob from kdb before register, so a fresh daemon recovers state the moment its first client connects; 7/7 incl. a real daemon-restart-over-the-socket integration test, proven with MemoryKv; the prod tikv-RawKv binding is a mechanical deploy follow-up). and slice 2b-ii-β-3-1 (the canonical anti_entropy reconciliation logic — DocCrdt::merkle_digest() builds a koder-crdt Merkle digest by hashing each constituent sub-CRDT via CanonicalDigest, NOT the non-deterministic to_bytes blob, so equal state hashes equal regardless of HashMap order; CollabService::
reconcile is digest-gated — equal root() ⇒ skip the merge, the cheap common case — over merge_remote; 32/32, divergent replicas converge to an identical root and a second reconcile no-ops), and slice 2b-ii-β-3-2-i (the selective-transfer data primitive DocCrdt::extract(keys) — builds a partial document of just the sub-CRDT entries compute_diff flags as disputed, so a peer ships only the differing entries; correct because per-entry merge is monotonic, so merge(extract(diff)) equals a whole-doc merge for strictly less data; 34/34, the selective key set is provably smaller than the whole-doc digest and converges identically), and slice 2b-ii-β-3-2-ii (the peer anti-entropy protocol koder-collabd/src/peer.rs — three serde messages DigestOfferKeyRequestStateResponse + four pure steps offer → select_keys →
serve → apply sequencing digest → compute_diff → extract → merge into a pull round; proven against an in-memory loopback like the client transport was against LoopbackHub — a round both directions converges two divergent replicas shipping only the differing entries, in-sync replicas exchange nothing, messages are JSON-wire-ready, a corrupt payload is a recoverable error; collabd green incl. 3 new peer tests), and slice 2b-ii-β-3-2-iii (the real daemon-to-daemon peer socket — a PeerFrame WebSocket wire carrying the pull round over the daemon's tokio-tungstenite stack: a ?role=peer upgrade routes to serve_peer, pull_once connects to a peer and runs Hello→Offer→Request→State→apply, and a peer_tick driven by --peer--peer-workspace --peer-interval flags reconciles shared workspaces with no central coordinator; proven with two real OS processes — tests/peer_two_process.rs launches two koder-collabd binaries and asserts an edit on A reaches B purely through the anti-entropy socket), and slice 2b-ii-γ (the cross-language e2e — koder_kit/test/kvg/collab_cross_language_test.dart runs the real Dart WebSocketCollabTransport against the real Rust koder-collabd daemon: a Dart client inserts a node, a second Dart client reads it back through the daemon-served snapshot, proving the Dart CollabMessage/KadOp JSON and the Rust wire.rs interoperate byte-for-byte; gated on KODER_COLLABD_URL so it skips cleanly in the normal suite; verified live across the incus fleet — daemon on dev-linux-kdb, Dart test on dev-linux-dek over the network → all passed). ✅ KAD-013 is COMPLETE: the real-time collaboration feature (KAD-007) now has its full deployment plumbing, proven end to end with BOTH real implementations. Production hardening continues under KAD-014: slice 1 (live fan-out of a peer-merged delta) is done — CollabHub::apply_peer_state digest-gates a peer merge and, only when the canonical root changed, fans a fresh snapshot out to every client connected to that workspace (so a daemon-to-daemon anti-entropy change reaches already-connected clients live, not just on next join; clients merge it idempotently); peer::pull_once now drives it. KAD-014 slice 3 (peer discovery from config) is also done — the peer set is read from a TOML file (peer::PeerConfig/PeerSource, toml crate reuse-first) re-read every pull round, so a fleet is reconfigured by editing a file with no per-daemon restart (--peer-config <path>, precedence over --peer flags; missing/malformed → skip the round, no crash); proven by hot-reload unit tests + a second real two-process test where daemon B's peer set comes from --peer-config. Remaining KAD-014: kdb PersistStore wired into the binary (β-2 proven with MemoryKv; gated_by: deploy for the tikv endpoint) and RED metrics + traces (observability-first — done properly with the self-hosted infra/observe/ backend when the daemon deploys). The headless LoopbackHub + Dart WS transport tests are the behavioural oracle (same JSON wire). The backend (kadd) object-storage plane (KAD-012) is done: the generic tenant-scoped store (collection.go) offloads bodies ≥ a threshold to a BlobStore seam (MemBlobStore / HTTPBlobStore → infra/data/blob), the kdb row keeping a reference; documents + stencils share the one offload path, tenant-isolated (cross-tenant → 404). This is the durable store KAD-007 slice 3 (collab service) builds on. Document open/save (KAD-009) is done too: a DocumentStore seam (app/core/lib/src/store/ — MemoryDocumentStore for tests, HttpDocumentStore over the kadd /api/v1/documents REST API) + KadDocumentManager (open → fresh controller, save back the canonical source, content-exact dirty flag) closes the acceptance "save; reopen and see the result". The editor screen's File menu (New / Open / Save / Save as…, a dirty-dot title, an Open dialog listing the store's drawings) wires that lifecycle into the UI over the same seam. The only residual KAD-009 work is the native build-verify CI gate (infra) + bundle-asset stencil packs (← KAD-006). Remaining: the desktop + mobile shells (which wire the first-party packs as bundle assets), the Interact renderer/camera integration (coordinated with the Solid renderer), CRDT collab slices 3-6 (server service over koder-crdt + the kadd store + WS transport + presence + e2e), export (KAD-008 done — PNGSVGglTF/PDF). The product landing is LIVE*at https://kad.koder.dev (KAD-011, owner-approved 2026-06-08 — spec-conformant landing served by Koder Jet, HTTP/2 + auto-TLS; ClouDNS A → 177.93.107.211). It serves at the root for now; it relocates to /about when the Flutter web app (app/web) deploys to prod (per products.kmd §Padrão de URL).