Architecture
This page condenses ADR 0008 (docs/decisions/0008-editor-package.md). The ADR holds the reasoning.
The idea
Section titled “The idea”History and arrow-following need to know what changed, not what the user did. Shapes and bindings are immutable records, so an unchanged record is the same object. Each document also lists the ids written since it was derived from its base. Together they answer “what changed” in time proportional to the change, not to the document.
A feature is a pure function that returns the next Doc. It never builds a patch and never thinks about arrows.
| Value | In Editor |
Undoable |
|---|---|---|
Doc |
Yes | Yes |
| Selection | Yes | Recorded with each edit, never undone alone |
| History | Yes | No |
| Camera | No | No |
Editor is { doc, selection, history }. The camera is a separate value. A gesture keeps the editor it started from and commits from it on release. If the camera were inside Editor, a zoom during a drag would be rolled back on release.
The edit cycle
Section titled “The edit cycle”- A gesture starts from a clean
baseeditor, whose document has no touched ids. - Each pointer move builds
nextfrombase.docwith the document helpers, which record the ids they change intouched.preview(base, next)settles it for drawing. - On release,
commit(base, next)settlesnext, diffs it againstbase.docand pushes one history entry. The committed document is clean again. undoapplies the entry’s inverted patch. It does not settle again, because the patch already holds the settled arrows.
diff and changedShapeIds read only the touched ids, so the change detection behind each preview frame and each commit is O(changes), not O(shapes). settle also scans the bindings, so it is O(changes + bindings), and it returns at once when nothing changed. Writing a record copies the map it lives in. Persistent maps would remove that copy if it ever shows up in a profile.
- Docs come only from the helpers.
Docis branded, so a feature cannot build one that skipstouched. Start fromEMPTY_DOC,fromShapeDocumentor an editor’sdoc. nextderives frombase.doc.diff(before, after)trusts that every change inafterwent through a helper sincebefore.- No-op writes keep identity.
updateShapeskeeps the old object when the new one is data-equal, so a no-op edit pushes no entry and the drawing cache keeps hitting. - Committed documents are settled. Settling every id of a committed document again changes nothing. A property test checks this.
Pointer input
Section titled “Pointer input”The app shell converts each pointer event to page space with the camera and passes it to the session. The session is the only routing point:
- Press.
pointerDownbegins a gesture for the current tool. A gesture record holds only pointer data and ids minted once at begin, such as the new shape’s id. The session keeps the committed editor as the base. - Move.
pointerMoveasks the gesture’s feature for the next document from the base and storespreview(base, next). Frames never build on each other, so errors do not accumulate. - Release.
pointerUpcommits from the base once. A drag of any length is one entry.
cancel drops the gesture, and the base is shown again. perform cancels before it runs a command, so undo during a drag cannot mix the two.
Modules
Section titled “Modules”| Module | Job |
|---|---|
doc.ts |
Doc and the helpers that write it |
patch.ts |
diff, apply, invert |
settle.ts |
Bindings and bound arrows after any edit |
editor.ts |
Editor, preview, commit, undo, redo, select, load |
camera.ts |
Screen and page conversion, zoom about a point, culling |
input.ts |
Pointer and Tool |
env.ts |
EditorEnv, the injected id and seed ports |
gesture.ts |
The closed Gesture union and the routing of each kind to its feature |
session.ts |
Session and the pointer functions |
features/*.ts |
One file per feature: its gesture record, its begin and its step functions |
A new pointer feature adds its file in features/, one member to Gesture and GestureKind, and one branch in each routing function of gesture.ts. A new command adds its file and needs no routing: the shell runs it with perform.