Skip to content

fix(blocks): a run is drawn where the reader is standing, and a note lives on one level - #73

Merged
thethinkmachine merged 1 commit into
mainfrom
feat/blocks-playback-and-note-scope
Sep 4, 2026
Merged

fix(blocks): a run is drawn where the reader is standing, and a note lives on one level#73
thethinkmachine merged 1 commit into
mainfrom
feat/blocks-playback-and-note-scope

Conversation

@thethinkmachine

Copy link
Copy Markdown
Owner

Every path that reads App.states/App.transitions where it should read the view answers about a diagram that is not on screen — and it does so silently, because the model is still perfectly consistent. Six surfaces were still doing it, and each is invisible on any machine without a block on it.

The playback highlight

It failed as a whole rather than in part. getSimStepEdgeKeys built its key from each transition's own endpoints and updateSimCanvasHighlights looked its states up by their own ids — both exactly right about the machine, and both naming things the canvas has no node for the moment a block is on it. So from the first step a run touched a block, every mark went at once — no trail, no active edge, no travelling token, no arrival pulse — while the verdict stayed correct throughout, which is what made it read as "the animations are broken".

Both halves go through the projection now (viewEdgeKeyFor, visibleNodeIdFor), and three details follow: ids are resolved to drawn nodes before anything is compared, since several states inside one block are one box; what resolves to nothing is dropped rather than guessed at; and trailUpTo's cache carries the scope, because drilling in changes which node every step is shown by.

It reaches one level in

A box that lights up says "something in here" and nothing more. markPreviewRun writes onto elements the preview already built — drawPreview stamps __pvIndex and __pvEdgeD in loops it was already running — so nothing is rebuilt. The active edge is a second path rather than a class (a path per edge is a hundred elements per box), and no travelling token goes in there: at preview scale the dot would be larger than the states it travels between.

Two more that moved

  • slideBlockPreview() is one function with two callers now. blockPreviewKey is built from the members' positions, so every path that moved a box and ended in a full render rather than a drag frame left its diagram behind — Arrange, a paste, an undo, an arrow-key nudge, a collision push, the JFLAP importer's spreadForBlocks.
  • pulseSimNode asks what shape it is landing on. A circle of radius R growing 1.7× is right for a state and wrong twice over on a block.

The four that were not playback

drawnStateEl / drawnEdgeEl in js/render.js replace the same lookup written out per surface, and fix: syncSelectionClasses, where a crossing edge sat selected with nothing on screen saying so and Delete took it anyway; highlightNoteAnchors, which lit a note's states and none of its edges; and langHighlight/langHighlightSymbol, where hovering Q lit the handful of top-level states while the |Q| beside it counted every depth.

A note lives on one level

The one thing here that was not a lookup bug. A note is written somewhere, and until note.scope said so it was drawn at every level at once, positioned against a state that level does not draw — and dragging the box stranded it. Absent means the top level, so no serializer was edited and there is no migration (and deliberately not a SCHEMA_VERSION bump — assertReadableSchema throws on a document from the future). A note resolves its anchors against its own level, never the reader's, which is what keeps pruneNoteAnchorsRemoving giving one answer whoever asks. Ungrouping carries its notes to the parent; noteScopeOf is the safety net for every other way a block can vanish.

And viewGraph() is cheap again

A cache hit recomputed a derived block size, falling through to blockMembers() + blockChildren() — two unindexed filters — on what inlineBlock makes the ordinary path. This function is reached per edge label by edgeLabelsHidden(). A select-all over 2000 transitions measured 617ms with eight blocks against 7ms without.

Tests

tests/sim-blocks.test.js and tests/note-scope.test.js are new; view-graph, block-render and selection gained cases. The perf fix is pinned on object identity rather than a timer, because an equal box rebuilt every call is exactly the walk being guarded against.

npm test — 1830 passing, 0 failing.

…lives on one level

Every path that reads App.states/App.transitions where it should read the
view answers about a diagram that is not on screen, and does so silently,
because the model stays perfectly consistent. Six surfaces were still doing
that, and each is invisible on any machine without a block on it.

The playback highlight failed as a whole rather than in part: getSimStepEdgeKeys
built its key from a transition's own endpoints and updateSimCanvasHighlights
looked its states up by their own ids, so from the first step a run touched a
block every mark went at once while the verdict stayed correct. Both halves go
through the projection now (viewEdgeKeyFor, visibleNodeIdFor), ids are resolved
to drawn nodes before anything is compared, what resolves to nothing is dropped
rather than guessed at, and trailUpTo's cache carries the scope.

Marks reach one level in, since a box that lights up says "something in here"
and nothing more. markPreviewRun writes onto elements drawPreview already
built — __pvIndex and __pvEdgeD, stamped in loops it was already running — so
nothing is rebuilt; the active edge is a second path rather than a class, and
no travelling token goes in there.

slideBlockPreview is one function with two callers now: blockPreviewKey is
built from the members' positions, so Arrange, a paste, an undo, a nudge, a
collision push and the JFLAP importer's spread all moved a box and left its
diagram behind. pulseSimNode traces the node's own outline, so an arrival ring
on a block is a rect rather than a circle sweeping half the diagram.

drawnStateEl/drawnEdgeEl replace the same lookup written out per surface, and
fix three more: a crossing edge selected with nothing on screen saying so, a
note's anchors lighting its states and none of its edges, and the Language
panel highlighting the opposite of the |Q| printed beside it.

note.scope is the one thing here that was not a lookup bug. A note is written
somewhere, and until it said so it was drawn at every level at once against a
state that level does not draw. Absent means the top level, so no serializer
changed and there is no migration; a note resolves its anchors against its own
level rather than the reader's; ungrouping carries its notes to the parent
while noteScopeOf is the safety net for every other way a block can vanish.

And viewGraph() is cheap again: a cache hit recomputed a derived block size,
falling through to two unindexed filters on the ordinary path, which a
select-all over 2000 transitions measured at 617ms with eight blocks against
7ms without.
Copilot AI lite review requested due to automatic review settings September 4, 2026 17:07
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@thethinkmachine
thethinkmachine merged commit 33c2f37 into main Sep 4, 2026
5 checks passed
@thethinkmachine
thethinkmachine deleted the feat/blocks-playback-and-note-scope branch September 4, 2026 17:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants