Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ Lean on the pre-commit hook: run `git commit ...` and fix reported problems rath
- Body: 1-2 paragraphs explaining "why", highlighting assumptions and non-obvious decisions
- DO NOT use "fixes"/"resolves" or emoji in commit messages

## Pull Requests

- The PR description MUST include a GitHub closing keyword -- `Fixes #<n>` (or `Closes #<n>`) -- for every issue the PR resolves, so merging auto-closes those issues. List one per line when a PR resolves several.
- Keep the closing keywords in the PR description, NOT in commit messages (commit messages deliberately avoid "fixes"/"resolves" per the style above; the auto-close should fire once, on merge, from the PR).
- The body should explain "why" and call out non-obvious decisions, mirroring the commit-message guidance.

## Hard Rules

IMPORTANT: Simple, general, testable, maintainable code is better than preserving an interface. There are NO places where VM bytecode is serialized to disk; backwards compatibility is ONLY needed for protobufs.
Expand Down
2 changes: 2 additions & 0 deletions src/diagram/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ Material-style UI component library (40+ components): Accordion, AppBar, Button,

Most project/engine invariants now live in `ProjectController` (`project-controller.ts`); the Editor-side ones describe rendering and UI-state consequences. Both are load-bearing.

- **Live viewport ownership** (Canvas, issue #707): during a viewport gesture -- drag-pan, momentum coast, wheel/trackpad pan+zoom, pinch -- the Canvas holds offset+zoom in local `liveViewport` state and renders from it, notifying the host (`onViewBoxChange`) exactly **once, on settle** (pan release with no momentum, the momentum coast's natural end, pinch exit, or the `DEFERRED_COMMIT_DELAY_MS` trailing-debounce for wheel). The former per-frame engine round-trip is gone. `getCanvasOffset()`/`getCanvasZoom()` resolve from `liveViewport` when set, else `props.view`; the single settle commit goes through `commitLiveViewport()`, which sources viewBox width/height from the live `svgSize` so a mid-gesture resize settles with the current dimensions. The viewport math is the pure `drawing/viewport.ts`; the shell owns the rAF loop, the `refs.deferredCommitTimer` debounce, and screen->canvas mapping. A drag-pan anchors against `refs.panBaseOffset` (captured at press) so a pan that interrupts a coast does not snap back to the last committed viewBox. Interruptions (a new pointer-down / wheel) never commit and never clear `liveViewport`: the new gesture inherits it (via `panBaseOffset` / the pinch reference reads) and commits the combined result. The `refs.deferredCommitTimer` is the safety net for an **orphaned** live viewport -- one left by a wheel gesture (no end event) or by a coast a non-viewport press interrupted: its guarded callback commits only when no pan/pinch/coast is active, so a plain click between a scroll/coast and the timer still persists the viewport instead of stranding it, with no double commit. A mid-gesture resize updates only `svgSize` (the gesture keeps the offset; no re-centering shift to fight or be discarded); an idle resize re-centers and commits immediately. Embedded mode is viewport-inert (it draws to tight element bounds and ignores viewBox/zoom). These gestures still ultimately call the controller's `queueViewUpdate`, just once per gesture instead of per frame.
- **External view overrides a live gesture** (Canvas, issue #707): a `useEffect` keyed on `[props.view, liveViewport]` compares `props.view`'s offset/zoom VALUE against `refs.viewBaseline` (tracked while idle). While a gesture is live, `props.view` is expected to stay put (a gesture never commits mid-flight), so any value change seen with `liveViewport` still set is external (centerVariable, module navigation, undo) and drops the live viewport + cancels pending momentum/deferred commits -- the external view wins, with no stray commit. A self-commit clears `liveViewport` in the same React commit as its optimistic `props.view` update, so it is never misread as external; comparing by value (not snapshot identity) ignores a content-equal republished view. If a pointer-driven viewport gesture (drag-pan or pinch) is still physically in progress when this fires, it is also abandoned (interaction -> idle; `panBaseOffset`/`mouseDownPoint`/`pointerId`/`activePointers` cleared) -- otherwise a continued move would recreate `liveViewport` from the stale press-time anchor and the pointer-up could commit that abandoned gesture back over the external view.
- **Optimistic view updates** (controller): `updateView()`/`queueViewUpdate()` call `applyOptimisticView()` (synchronous snapshot replace of the active model's view + `projectVersion += 0.001`) *before* awaiting the engine round-trip. Any new view-modifying handler must go through these controller methods to avoid flicker.
- **updateProject preserves the live view** (controller): `ProjectController.updateProject()` rebuilds `project` from the engine's serialized JSON, then merges via `preserveLiveView()` so the active model's view comes from the live snapshot (the most recent optimistic view). Without this, a slow engine round-trip racing with a newer pan/move would snap the diagram back to the engine's older view. The live view is round-tripped through JSON to re-link element `var` refs and stock inflow/outflow UIDs against the incoming variables.
- **View-only updates never record undo history** (controller): the `queueViewUpdate` path calls `updateProject(serialized, { recordHistory: false, scheduleSave: false })` -- it refreshes `project` and bumps `projectVersion` but must not touch `projectHistory`/`projectOffset`: viewBox/zoom are serialized into the protobuf, so recording them would let a single momentum flick evict every real edit from the `MaxUndoSize` (5) buffer. Real edits go through `advanceProjectHistory` (project-history.ts), which discards the redo branch when editing after an undo.
Expand Down
Loading
Loading