Skip to content

WIP Add GPU transitions#228

Open
Stukova wants to merge 18 commits into
mainfrom
feature/gpu-transitions
Open

WIP Add GPU transitions#228
Stukova wants to merge 18 commits into
mainfrom
feature/gpu-transitions

Conversation

@Stukova
Copy link
Copy Markdown
Member

@Stukova Stukova commented Apr 22, 2026

Adds GPU-driven transitions for point positions, colors, sizes, and link colors/widths. Also makes enableSimulation runtime-switchable so simulation can be toggled without recreating the graph.

Animated transitions

When you update graph data and call render(), the change now animates instead of snapping. Position transitions interpolate on the GPU; color/size/width transitions blend in the draw shader.

const graph = new Graph(div, {
  transitionDuration: 800,                   // ms, default 800; set 0 to snap
  transitionEasing: TransitionEasing.CubicInOut,
})

// First render — establishes the initial layout, snaps into place.
graph.setPointPositions(initialPositions)
graph.setPointColors(initialColors)
graph.render()

// Subsequent updates animate from the current state to the new one.
graph.setPointPositions(nextPositions)
graph.setPointColors(nextColors)
graph.render()

All updates queued between two render() calls animate together as one synchronized cycle.

Notes

  • Interactions during transitions. Drag start is blocked during point position and point size transitions; hover detection is suppressed during point size transitions. Position and link hover keep working.
  • fitView during a position transition frames the target positions (the latest setPointPositions argument), not the interpolated ones currently on screen. Same for fitViewByPointIndices.
  • start() and unpause() interrupt active position transitions (onTransitionEnd(true) fires); color/size/link transitions keep playing.
  • TransitionEasing is exported from the package.

Lifecycle callbacks

const graph = new Graph(div, {
  onTransitionStart: () => { /* cycle began */ },
  onTransition: (progress) => { /* eased progress in [0, 1] */ },
  onTransitionEnd: (interrupted) => { /* cycle finished or was cut short */ },
})

Runtime simulation toggle

enableSimulation is no longer init-only — flip it at any time:

graph.setConfigPartial({ enableSimulation: false })  // freeze layout, keep rendering
graph.setConfigPartial({ enableSimulation: true })   // re-enable forces

New story

src/stories/transition/ — a 200k-point cloud sampled from a painting, auto-looping between the picture layout and a sequence of tile scatters.

Screenshot 2026-04-27 at 20 05 26

Breaking changes

  • Transitions are on by default. setPointPositions(...) + render() now animates over 800ms instead of snapping immediately. To keep the old snap behavior, initialize with transitionDuration: 0, or disable it for a single update:
const graph = new Graph(div, {
  transitionDuration: 0,
})
  • Position transitions auto-pause the simulation. When setPointPositions() triggers a transition while the simulation is running, the simulation pauses for the transition and stays paused afterwards — calling setPointPositions() is treated as intent to explore a specific layout, not feed the physics. Call graph.unpause() to resume.

  • enableSimulation is no longer init-only. Previously listed as init-only and ignored by setConfig(). Now applied at runtime. Code that relied on it being a no-op after init will see real behavior changes.

Contributor tooling: history/ and /history

This PR also introduces a lightweight place to capture the why behind non-trivial changes, separate from commit messages:

  • New history/ folder with README.md (conventions) and PROMPT.md (LLM template). The first entry, history/2026/2026-04-22-gpu-transitions.md, documents this PR.
  • New /history Claude Code slash command at .claude/skills/history/SKILL.md:
    • /history <why> — drafts a new entry from recent commits.
    • /history --update — revises the latest entry in place when follow-up commits extend the same topic, without re-asking for a "why".
  • CONTRIBUTING.md points to all of the above.

Summary by CodeRabbit

  • New Features

    • GPU-animated transitions for point positions, colors, sizes, and link widths/colors with configurable duration (default 800ms) and easing; set duration=0 to snap.
    • New transition lifecycle callbacks: onTransitionStart, onTransition(progress), onTransitionEnd(interrupted).
    • Easing control exported and a Storybook transitions demo added.
  • Behavior Changes

    • enableSimulation is runtime-toggleable.
    • Position transitions auto-pause simulation; resuming interrupts active transitions.
    • Hover detection suppressed during point-size transitions; fitView targets use transition target positions.
  • Documentation

    • Guides, migration notes, and API docs updated with transition details.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

Important

Review skipped

This PR was authored by the user configured for CodeRabbit reviews. CodeRabbit does not review PRs authored by this user. It's recommended to use a dedicated user account to post CodeRabbit review feedback.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ea34670a-5cc4-4ad9-a56e-d5820721969c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a GPU-driven Transition system integrated into Graph, Points, Lines, and Drag; introduces per-property queued transitions with duration/easing and lifecycle callbacks; updates shaders/attributes to source/target pairs for interpolation; makes enableSimulation runtime-toggleable; adds stories, docs, and helpers. (≤50 words)

Changes

Cohort / File(s) Summary
Config & Defaults
src/config.ts, src/variables.ts
Add transitionDuration, transitionEasing, lifecycle callbacks (onTransitionStart/onTransition/onTransitionEnd); update enableSimulation doc to be runtime-toggleable; re-export TransitionEasing; set new defaults.
Core / Graph API
src/index.ts
Integrate Transition into render lifecycle: setters enqueue transitions, render() starts/steps transitions, pause/resume semantics adjusted, fitView targets transition targets, and applyEnableSimulationConfigChange() handles runtime toggles; destroy() aborts transitions.
Transition Module
src/modules/Transition/index.ts
New Transition class and TransitionProperty/TransitionEasing enums: queue/active semantics, start/step/end/abort, progress with easing, and lifecycle callbacks.
Points — Core, Shaders & Utils
src/modules/Points/...
src/modules/Points/index.ts, src/modules/Points/draw-points.vert, src/modules/Points/interpolate-position.frag, src/modules/Points/position-utils.ts
Switch to source/target position/size/color inputs; add GPU interpolation pass and shader; manage source/target FBOs/textures and CPU snapshots; update attribute buffer helpers; add transition APIs (setTransitionProgress, interpolatePosition, resource lifecycle methods); updatePositions() now returns boolean.
Lines — Core & Shader
src/modules/Lines/index.ts, src/modules/Lines/draw-curve-line.vert
Introduce source*/target* color/width attributes and cached previous CPU snapshots; add transitionProgress and animate flags; topology-aware buffer updates and setTransitionProgress() API; update shader to mix attributes.
Drag Integration
src/modules/Drag/index.ts
Inject Transition into Drag, add public transition field and constructor param; prevent drag initiation while positions or point-size transitions are active.
GraphData
src/modules/GraphData/index.ts
Record sourcePointsNumber and targetPointsNumber in updatePoints() to support topology changes during transitions.
Render Flow & Setters
src/index.ts, src/modules/*
Setters enqueue TransitionProperty entries; render starts/steps transitions each frame, interpolates GPU positions, forwards progress/flags to Points/Lines, auto-pauses simulation for pending position transitions, suppresses hover during size transitions, and handles interruption on simulation toggles.
Shaders & GPU utilities
src/modules/Points/interpolate-position.frag, src/modules/Points/draw-points.vert, src/modules/Lines/draw-curve-line.vert
New fragment shader for position interpolation; vertex shaders updated to accept source/target pairs and transition uniforms (progress + animate flags) with UBO/non-UBO paths.
Stories & Examples
src/stories/..., src/stories/transition/*
Add transition demo, helpers, CSS and stories; update Storybook docs and API reference to document transitions, callbacks, migration notes, and examples; some stories opt out by setting transitionDuration: 0.
Misc / Declarations & Docs
src/declaration.d.ts, README.md, migration-notes.md, history/**, CONTRIBUTING.md
Add *.jpg module declaration; update README/migration docs; add history entries, prompt/skill docs, contributing guidance, and reclassify enableSimulation behavior.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant Graph as Graph
    participant Transition as Transition
    participant Points as Points
    participant Lines as Lines
    participant GPU as GPU/Canvas

    Note over App,Graph: App enqueues updates via setters
    App->>Graph: setPointPositions/Colors/Sizes/Link*(...)
    Graph->>Transition: queue(...) (properties)
    App->>Graph: render()
    Graph->>Transition: start()
    Transition->>Transition: onTransitionStart()

    loop per frame
        Graph->>Transition: step(now)
        Transition-->>Graph: progress (eased)
        Graph->>Points: setTransitionProgress(progress, animateFlags)
        Graph->>Lines: setTransitionProgress(progress, animateFlags)
        alt positions transition active
            Graph->>Points: interpolatePosition(progress)
            Points->>GPU: GPU interpolation pass (interpolate-position.frag)
        end
        Points->>GPU: draw points (draw-points.vert mixes attrs)
        Lines->>GPU: draw lines (draw-curve-line.vert mixes attrs)
        GPU->>Graph: frameRendered
    end

    Transition->>Transition: onTransitionEnd(interrupted=false)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • rokotyan

Poem

🐇 I nibble frames and blend their hues,

I queue the steps and smooth the cues,
Durations hum and easing sighs,
Pixels hop as progress flies,
A tiny rabbit claps its eyes.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'WIP Add GPU transitions' is directly related to the main changeset: introducing GPU-driven transitions for point positions, colors, sizes, and link colors/widths. However, the 'WIP' prefix and lack of detail make it incomplete rather than a finalized, clear summary. Remove the 'WIP' prefix and clarify the scope, for example: 'Add GPU transitions for points and links' or 'Implement GPU-driven transitions with runtime-switchable simulation'.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (3)
src/index.ts (1)

255-257: Prefer constructor injection over post-construction field mutation.

this.points.transition = this.transition at line 256 sets a public field on Points immediately after it's constructed, instead of passing transition through the constructor like Drag does (line 61). This pattern leaks a two-step initialization contract (consumers must remember to set transition or the transition-aware code paths in Points silently no-op) and forces transition to be a mutable public field. Passing transition into the Points constructor alongside device/config/store/graph would be more consistent and safer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 255 - 257, The code mutates this.points.transition
after construction which leaks a two-step init; update the Points class
constructor to accept a Transition parameter (same type as this.transition),
update the instantiation site here to pass this.transition into new
Points(device, this.config, this.store, this.graph, this.transition) and remove
the post-construction assignment (this.points.transition = this.transition);
also update any other call sites and the Points type signature accordingly
(follow the same pattern used by Drag) so transition is provided via constructor
injection and kept immutable.
src/modules/Lines/index.ts (2)

986-998: Mid-transition interruption causes a brief visual jump.

When updateAttributeBuffers is called with sameCount while a previous transition is still in flight, the swap makes the new source equal to the prior cycle's fully-populated target (i.e., the end state of the previous animation), not the currently on-screen interpolated value. Since transition.start() then resets progress to 0, the first frame after the setter snaps from the current interpolated color/width to the prior target before animating toward the new target.

This is likely acceptable as an initial implementation, but worth a doc note on setLinkColors/setLinkWidths behavior under rapid re-entries, or alternatively reading back the currently displayed values to seed the new source so the animation starts from where the eye actually is.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/Lines/index.ts` around lines 986 - 998, The swap logic in
updateAttributeBuffers currently reuses the prior target as the new source
causing a visual snap when a previous transition is mid-flight; detect an active
transition (e.g., inspect the transition object used by
setLinkColors/setLinkWidths and check transition.progress is between 0 and 1)
and instead seed the new source with the currently displayed/interpolated values
by computing a lerp between the existing sourceBuffer data and targetBuffer data
using transition.progress (or read back the currently-displayed attribute if
available), then write that interpolated array into nextSource before returning
so the new transition starts from the on-screen state rather than the prior
target.

132-139: Pre-allocated colorBuffer/widthBuffer are now dead and leaked.

Since the drawCurveCommand attribute bindings switched to sourceColor/targetColor and sourceWidth/targetWidth (lines 221–224), these colorBuffer/widthBuffer preallocations are no longer bound to any shader attribute. They have two remaining side effects, both undesirable:

  1. The lazy-init guards in draw() (lines 375–376, if (!this.colorBuffer) this.updateColor() and if (!this.widthBuffer) this.updateWidth()) are now effectively dead — colorBuffer/widthBuffer are always defined by initPrograms, so these guards never trigger. Initial population relies entirely on the isLinkColor/WidthUpdateNeeded flags set by setLinks/setLinkColors/setLinkWidths via create().
  2. When updateColor()/updateWidth() eventually run, they reassign this.colorBuffer = target / this.widthBuffer = target (lines 596, 620) without destroying the previously-preallocated buffer, leaking one GPU buffer allocation per field until device destruction.

Consider either removing the preallocation entirely and repurposing the guards to call updateColor()/updateWidth() to initialize the source/target buffers, or dropping colorBuffer/widthBuffer from the class now that they're just shadow aliases of targetColorBuffer/targetWidthBuffer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/Lines/index.ts` around lines 132 - 139, Preallocated
this.colorBuffer/this.widthBuffer in initPrograms are unused and leak; remove
their creation and the class-level aliases so the code only uses
targetColorBuffer/targetWidthBuffer (and the shader bindings
drawCurveCommand/sourceColor/targetColor and sourceWidth/targetWidth). Update
draw() to stop checking the dead guards (if (!this.colorBuffer) / if
(!this.widthBuffer)) and instead rely on the existing
isLinkColor/WidthUpdateNeeded flags and updateColor()/updateWidth() to
initialize or refresh targetColorBuffer/targetWidthBuffer (ensure
create()/setLinks/setLinkColors/setLinkWidths still set those flags), and delete
any assignments that reassign colorBuffer/widthBuffer without destroying the
previous buffer; if you prefer keeping aliases, ensure
updateColor()/updateWidth() destroy the old GPU buffer before reassigning to
avoid leaks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/index.ts`:
- Around line 740-742: The guard currently calls transition.abort() when
this.transition.isPending && !this.points?.currentPositionTexture, but abort()
clears the whole queue (dropping unrelated pending updates); change this to only
remove or skip pending Positions updates: add/use a narrow Transition API (e.g.,
Transition.isPendingFor('Positions') or Transition.dequeue('Positions')) or
implement logic in the caller to inspect the pending queue and remove only the
Positions entry when currentPositionTexture is missing; update the check around
this.transition.isPending and this.points?.currentPositionTexture to call the
new targeted method instead of transition.abort(), and adjust Transition
(methods like isPendingFor, dequeue, or removePendingByType) to support
selective removal without clearing other pending/active cycles.

In `@src/modules/Drag/index.ts`:
- Around line 12-14: Update the subject((event) => ...) drag gate to also block
when a transition is pending: in the callback currently checking
this.transition.isActive, extend the check to include the transition's pending
flag (e.g., this.transition.isPending or this.transition.pending depending on
the Transition API you use) so the early-return becomes something like if
(this.transition.isActive || this.transition.isPending) return undefined; keep
the existing checks for this.store.hoveredPoint and
!this.store.isSpaceKeyPressed unchanged.

In `@src/modules/Points/index.ts`:
- Around line 2348-2369: interpolatePosition writes updated GPU positions into
currentPositionFbo but does not invalidate CPU/consumer caches, so after the
render pass you must mark position-dependent caches stale; after
renderPass.end() in interpolatePosition(), call cache invalidation methods such
as this.invalidateTrackedPositionsCache() and
this.invalidateClusterCentroidsCache() (or set flags like
this.trackedPositionsStale = true and this.clusterCentroidsStale = true if you
use boolean invalidation), ensuring any consumers/readbacks check those
methods/flags and recompute from this.currentPositionFbo when stale; add the
corresponding methods/flag checks elsewhere (e.g., where tracked positions or
centroids are read) to trigger a refresh.
- Around line 939-950: The code currently falls back to zero-filled arrays
causing transparent/zero-size points; change the fallbacks in the color and size
preparation to use the module's configured defaults instead of zeros: when
building colorData use data.pointColors ?? new Float32Array((data.pointsNumber
?? 0) * 4).fill(<use module default RGBA, e.g. this.pointColor or
this.config.pointColor>) and when building sizeData (the block around
updateAttributeBuffers for sizes at ~1047) use data.pointSizes ?? new
Float32Array(data.pointsNumber ?? 0).fill(<use module default size, e.g.
this.pointSize or this.config.pointSize>); keep using updateAttributeBuffers,
sourceColorBuffer, targetColorBuffer, previousColorData and colorBuffer (and the
analogous size buffers) but replace the zero-fills with the canonical defaults
so missing attributes render with visible defaults.
- Around line 2190-2213: The resize/recreate branches in
createOrUpdateVelocityTexture(), createOrUpdateSimulationResources(), and
createOrUpdatePositionTextures() currently destroy textures before their
attached framebuffers (e.g., velocityTexture then velocityFbo), which violates
the documented destruction order; update each branch to destroy the framebuffer
(e.g., velocityFbo) first if it exists and is not destroyed, then destroy the
texture (e.g., velocityTexture) to match "Framebuffers → Textures" ordering, and
apply the same swap for all similarly named pairs (positionFbo/positionTexture,
any simulationFbo/simulationTexture) throughout the function bodies.

In `@src/stories/3`. api-reference.mdx:
- Around line 59-61: The sentence about enableSimulation is ambiguous about
whether it is part of the "reset to defaults when set to undefined" rule; reword
the paragraph to clarify contrast with setConfig-init-only fields by explicitly
stating that properties set to undefined are reset to defaults, and separately
note that enableSimulation can be toggled at runtime without recreating the
graph instance (reference enableSimulation and setConfig to locate the text).

---

Nitpick comments:
In `@src/index.ts`:
- Around line 255-257: The code mutates this.points.transition after
construction which leaks a two-step init; update the Points class constructor to
accept a Transition parameter (same type as this.transition), update the
instantiation site here to pass this.transition into new Points(device,
this.config, this.store, this.graph, this.transition) and remove the
post-construction assignment (this.points.transition = this.transition); also
update any other call sites and the Points type signature accordingly (follow
the same pattern used by Drag) so transition is provided via constructor
injection and kept immutable.

In `@src/modules/Lines/index.ts`:
- Around line 986-998: The swap logic in updateAttributeBuffers currently reuses
the prior target as the new source causing a visual snap when a previous
transition is mid-flight; detect an active transition (e.g., inspect the
transition object used by setLinkColors/setLinkWidths and check
transition.progress is between 0 and 1) and instead seed the new source with the
currently displayed/interpolated values by computing a lerp between the existing
sourceBuffer data and targetBuffer data using transition.progress (or read back
the currently-displayed attribute if available), then write that interpolated
array into nextSource before returning so the new transition starts from the
on-screen state rather than the prior target.
- Around line 132-139: Preallocated this.colorBuffer/this.widthBuffer in
initPrograms are unused and leak; remove their creation and the class-level
aliases so the code only uses targetColorBuffer/targetWidthBuffer (and the
shader bindings drawCurveCommand/sourceColor/targetColor and
sourceWidth/targetWidth). Update draw() to stop checking the dead guards (if
(!this.colorBuffer) / if (!this.widthBuffer)) and instead rely on the existing
isLinkColor/WidthUpdateNeeded flags and updateColor()/updateWidth() to
initialize or refresh targetColorBuffer/targetWidthBuffer (ensure
create()/setLinks/setLinkColors/setLinkWidths still set those flags), and delete
any assignments that reassign colorBuffer/widthBuffer without destroying the
previous buffer; if you prefer keeping aliases, ensure
updateColor()/updateWidth() destroy the old GPU buffer before reassigning to
avoid leaks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0c779d55-49ee-40ad-8636-766d2d8b7f05

📥 Commits

Reviewing files that changed from the base of the PR and between e2a8423 and e2af399.

📒 Files selected for processing (14)
  • src/config.ts
  • src/index.ts
  • src/modules/Drag/index.ts
  • src/modules/GraphData/index.ts
  • src/modules/Lines/draw-curve-line.vert
  • src/modules/Lines/index.ts
  • src/modules/Points/draw-points.vert
  • src/modules/Points/index.ts
  • src/modules/Points/interpolate-position.frag
  • src/modules/Points/position-utils.ts
  • src/modules/Transition/index.ts
  • src/stories/2. configuration.mdx
  • src/stories/3. api-reference.mdx
  • src/variables.ts

Comment thread src/index.ts Outdated
Comment thread src/modules/Drag/index.ts
Comment thread src/modules/Points/index.ts Outdated
Comment thread src/modules/Points/index.ts
Comment thread src/modules/Points/index.ts
Comment thread src/stories/3. api-reference.mdx
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/stories/transition/point-data.ts (1)

9-16: Consider setting crossOrigin for user-supplied image URLs.

loadPointData(imageUrl) is exported and accepts an arbitrary URL. If a caller passes a cross-origin URL without proper CORS headers, the canvas will be tainted and ctx.getImageData in sampleImageToPointColors will throw a SecurityError. The bundled default image is same-origin so the demo works today, but setting img.crossOrigin = 'anonymous' before assigning src makes the helper safe for external URLs (and is a no-op for same-origin).

🛡️ Proposed change
 function loadImage (url: string): Promise<HTMLImageElement> {
   return new Promise((resolve, reject) => {
     const img = new Image()
+    img.crossOrigin = 'anonymous'
     img.onload = (): void => resolve(img)
     img.onerror = (): void => reject(new Error(`Failed to load image: ${url}`))
     img.src = url
   })
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stories/transition/point-data.ts` around lines 9 - 16, The loadImage
helper can cause canvas tainting for cross-origin images; modify the loadImage
function to set img.crossOrigin = 'anonymous' before assigning img.src so
external URLs can be fetched without CORS tainting (it is a no-op for
same-origin); this protects downstream code such as sampleImageToPointColors and
any exported loadPointData that relies on ctx.getImageData.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/stories/transition/point-transition.stories.ts`:
- Around line 16-18: The story's meta object (meta: Meta<CosmosStoryProps>) uses
title: 'Examples/Beginners' which miscategorizes this transition story; update
the title string to a dedicated transitions category such as
'Examples/Transitions/Point Position Transition' (or similar) so the story
appears under Examples → Transitions → Point Position Transition in Storybook
and does not collide with the existing Beginners section.

In `@src/stories/transition/point-transition.ts`:
- Around line 61-64: The button label is set to 'Pause' prematurely causing a
possible double-start; change the initial label for pausePlayAction to 'Play' to
match the initial not-playing state, and update the label only when the loop
actually transitions to playing (e.g., in the click handler when you call
startLoop() and in the requestAnimationFrame callback when you confirm the loop
started); also ensure startLoop() is guarded by the playing flag in both the
click handler and the rAF callback so startLoop() cannot be invoked twice.
- Around line 113-132: The rAF callback unconditionally calls startLoop(), which
can overwrite an earlier interval started by the pausePlayAction click handler
and leak timers; modify the rAF callback to only call startLoop() when the loop
is not already running (check loopIntervalId === undefined or use a dedicated
boolean like isLoopStarted), and make startLoop() itself idempotent by setting
that flag (or only assigning loopIntervalId when it is currently undefined) so
repeated calls from the click handler or rAF cannot double-schedule intervals;
ensure stopLoopTimer() clears the flag and loopIntervalId when stopping.

---

Nitpick comments:
In `@src/stories/transition/point-data.ts`:
- Around line 9-16: The loadImage helper can cause canvas tainting for
cross-origin images; modify the loadImage function to set img.crossOrigin =
'anonymous' before assigning img.src so external URLs can be fetched without
CORS tainting (it is a no-op for same-origin); this protects downstream code
such as sampleImageToPointColors and any exported loadPointData that relies on
ctx.getImageData.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b1679608-c291-4990-bcb8-fef2c39b27c4

📥 Commits

Reviewing files that changed from the base of the PR and between e2af399 and a9272fd.

⛔ Files ignored due to path filters (1)
  • src/stories/transition/horsewoman-by-bryullov-1832.jpg is excluded by !**/*.jpg
📒 Files selected for processing (8)
  • README.md
  • migration-notes.md
  • src/declaration.d.ts
  • src/stories/transition/point-data.ts
  • src/stories/transition/point-transition.stories.ts
  • src/stories/transition/point-transition.ts
  • src/stories/transition/transition-helpers.ts
  • src/stories/transition/transition.css
✅ Files skipped from review due to trivial changes (3)
  • src/declaration.d.ts
  • src/stories/transition/transition.css
  • migration-notes.md

Comment thread src/stories/transition/point-transition.stories.ts
Comment thread src/stories/transition/point-transition.ts
Comment thread src/stories/transition/point-transition.ts Outdated
Stukova added 3 commits April 23, 2026 15:01
Signed-off-by: Stukova Olya <stukova.o@gmail.com>
Signed-off-by: Stukova Olya <stukova.o@gmail.com>
- Add history/ folder with README, LLM prompt, and first entry covering
  GPU transitions and runtime simulation toggle (commits e2af399, a9272fd)
- Add /history skill to auto-draft entries from git context
- Update CONTRIBUTING.md to point contributors to history/ for non-trivial changes

Signed-off-by: Stukova Olya <stukova.o@gmail.com>
@Stukova Stukova force-pushed the feature/gpu-transitions branch from d3529ff to 6b540da Compare April 23, 2026 10:08
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/modules/Lines/index.ts (1)

138-145: ⚠️ Potential issue | 🟠 Major

Leaked/obsolete initial colorBuffer and widthBuffer allocations.

The model no longer binds color/width attributes directly — it uses sourceColor/targetColor and sourceWidth/targetWidth (see lines 235–249). The standalone buffers created here are therefore never attached to any draw pipeline, and the first call to updateColor()/updateWidth() overwrites this.colorBuffer/this.widthBuffer with the new target buffer via alias assignment (lines 617, 641) without destroying the original — a GPU buffer leak per module init.

Two consequences worth addressing together:

  1. Drop the pre-allocation (it's dead).
  2. Reconsider whether keeping colorBuffer/widthBuffer as aliases is necessary at all; if they're only used for the if (!this.colorBuffer) this.updateColor() guards in draw()/findHoveredLine(), switching those to check this.targetColorBuffer/this.targetWidthBuffer removes the aliasing (and the leak vector) entirely.
Proposed fix
-    this.colorBuffer ||= device.createBuffer({
-      data: new Float32Array(linksNumber * 4),
-      usage: Buffer.VERTEX | Buffer.COPY_DST,
-    })
-    this.widthBuffer ||= device.createBuffer({
-      data: new Float32Array(linksNumber),
-      usage: Buffer.VERTEX | Buffer.COPY_DST,
-    })

And in draw() / findHoveredLine() (and wherever colorBuffer/widthBuffer are read):

-    if (!this.colorBuffer) this.updateColor()
-    if (!this.widthBuffer) this.updateWidth()
+    if (!this.targetColorBuffer) this.updateColor()
+    if (!this.targetWidthBuffer) this.updateWidth()

Then this.colorBuffer/this.widthBuffer (and their destroy blocks) can be removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/Lines/index.ts` around lines 138 - 145, The pre-allocated
this.colorBuffer and this.widthBuffer are dead and leak because attributes now
use sourceColor/targetColor and sourceWidth/targetWidth; remove the initial
allocations for colorBuffer/widthBuffer in the constructor/init code, stop
aliasing them in updateColor()/updateWidth(), and update guards in draw() and
findHoveredLine() (and any other checks) to test this.targetColorBuffer and
this.targetWidthBuffer instead of this.colorBuffer/this.widthBuffer; finally
remove any remaining uses/destroys of the old this.colorBuffer/this.widthBuffer
fields so only the target/source buffers are created and destroyed by their
existing update/destroy logic.
♻️ Duplicate comments (4)
src/modules/Points/index.ts (3)

2209-2233: ⚠️ Potential issue | 🟠 Major

Destroy attached framebuffers before replacing their textures.

These recreate paths still destroy textures before framebuffers that reference them, which conflicts with the documented lifecycle order and can leave framebuffer attachments dangling during resize/reallocation.

🛠️ Proposed ordering pattern
-      if (this.velocityTexture && !this.velocityTexture.destroyed) {
-        this.velocityTexture.destroy()
-      }
       if (this.velocityFbo && !this.velocityFbo.destroyed) {
         this.velocityFbo.destroy()
       }
+      if (this.velocityTexture && !this.velocityTexture.destroyed) {
+        this.velocityTexture.destroy()
+      }

Apply the same ordering to sourcePositionFbo/sourcePositionTexture, targetPositionFbo/targetPositionTexture, currentPositionFbo/currentPositionTexture, and previousPositionFbo/previousPositionTexture.

Also applies to: 2252-2297, 2482-2529

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/Points/index.ts` around lines 2209 - 2233, The teardown/recreate
sequence destroys textures before their attached framebuffers causing dangling
attachments; change the order so you destroy each framebuffer (e.g.,
velocityFbo, sourcePositionFbo, targetPositionFbo, currentPositionFbo,
previousPositionFbo) before destroying or replacing its corresponding texture
(velocityTexture, sourcePositionTexture, targetPositionTexture,
currentPositionTexture, previousPositionTexture), and apply the same ordering in
the recreate blocks (the createTexture/copyImageData/createFramebuffer
sequences) found around the velocity and the other position texture sections.

2368-2389: ⚠️ Potential issue | 🟠 Major

Invalidate position caches after interpolation.

interpolatePosition() writes new positions into currentPositionFbo, but cached tracked positions and cluster centroids remain marked fresh, so readback APIs can return stale positions during/after a transition.

🐛 Proposed cache invalidation
     const renderPass = this.device.beginRenderPass({
       framebuffer: this.currentPositionFbo,
     })
     this.interpolatePositionCommand.draw(renderPass)
     renderPass.end()
+
+    this.areClusterCentroidsUpToDate = false
+    this.isPositionsUpToDate = false
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/Points/index.ts` around lines 2368 - 2389, interpolatePosition()
updates currentPositionFbo but doesn't mark cached readback data stale; after
the renderPass.end() call in interpolatePosition (the method named
interpolatePosition), invalidate the tracked positions and cluster centroids
caches so subsequent readback APIs won't return stale data — e.g. call the cache
invalidation methods or set the dirty flags associated with
trackedPositionsCache and clusterCentroidsCache (or set
this.trackedPositionsDirty = true / this.clusterCentroidsDirty = true)
immediately after renderPass.end().

940-955: ⚠️ Potential issue | 🟠 Major

Use configured defaults instead of zero transition fallbacks.

Missing color/size arrays currently become transparent colors and zero-size points, so transition buffers can make default-rendered points disappear.

🐛 Proposed fallback adjustment
-import { readPixels } from '@/graph/helper'
+import { getRgbaColor, readPixels } from '@/graph/helper'
-    const colorData = data.pointColors ?? new Float32Array((data.pointsNumber ?? 0) * 4).fill(0)
+    const defaultColor = getRgbaColor(this.config.pointDefaultColor)
+    const colorData = data.pointColors ?? Float32Array.from(
+      { length: (data.pointsNumber ?? 0) * 4 },
+      (_, i) => defaultColor[i % 4] as number
+    )
-    const sizeData = data.pointSizes ?? new Float32Array(data.pointsNumber).fill(0)
+    const sizeData = data.pointSizes ?? new Float32Array(data.pointsNumber).fill(this.config.pointDefaultSize)

Also applies to: 1059-1088

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/Points/index.ts` around lines 940 - 955, The fallback for missing
color/size data currently fills arrays with zeros causing transparent colors and
zero-size points; change updateColor to build the fallback Float32Array from the
configured defaults in this.store (use the actual store property for default
point color, e.g. a default RGBA tuple on this.store or this.store.defaults)
instead of new Float32Array(...).fill(0), and repeat that RGBA per-vertex;
likewise update the analogous size fallback (lines noted 1059-1088) to use the
configured default point size from this.store when data.pointSizes is missing so
transition buffers don’t collapse visuals; keep the existing calls to
updateAttributeBuffers and the same buffer variables (sourceColorBuffer,
targetColorBuffer, previousColorData, colorBuffer).
src/index.ts (1)

735-745: ⚠️ Potential issue | 🟠 Major

Scope transition pausing/aborting to position transitions only.

Line 735 pauses simulation for any pending transition, so color/size/width-only updates can unexpectedly stop the force simulation. Line 743 also aborts the entire queue when only Positions needs a current position texture, dropping unrelated pending transitions.

🐛 Proposed scoping fix
-    if (this.transition.isPending &&
+    if (this.transition.isPendingFor(TransitionProperty.Positions) &&
         this.store.isSimulationRunning &&
         this.config.transitionDuration > 0 &&
         !this._isFirstRenderAfterInit) {
       this.store.isSimulationRunning = false
       this.config.onSimulationPause?.()
     }
 
-    if (this.transition.isPending && !this.points?.currentPositionTexture) {
-      this.transition.abort()
+    if (this.transition.isPendingFor(TransitionProperty.Positions) && !this.points?.currentPositionTexture) {
+      this.transition.dequeue(TransitionProperty.Positions)
     }

Supporting API in Transition:

+  public isPendingFor (property: TransitionProperty): boolean {
+    return this.pendingProperties.has(property)
+  }
+
+  public dequeue (property: TransitionProperty): void {
+    this.pendingProperties.delete(property)
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 735 - 745, The pause and abort logic is currently
applied for any pending transition (using this.transition.isPending) which
incorrectly affects color/size/width-only updates; restrict these checks to
position-updating transitions only by adding/using a predicate on Transition
(e.g., a method or flag like affectsPositions or needsPositionTexture) and
update both conditionals: only pause the simulation and call
this.config.onSimulationPause when this.transition.isPending &&
this.transition.affectsPositions && this.store.isSimulationRunning &&
this.config.transitionDuration > 0 && !this._isFirstRenderAfterInit, and only
call this.transition.abort when this.transition.isPending &&
this.transition.affectsPositions && !this.points?.currentPositionTexture so
unrelated transitions are not paused/aborted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/index.ts`:
- Around line 1223-1225: When resuming the simulation, ending a transition with
this.transition.end(true) only clears state but doesn’t write the transition’s
target positions into currentPositionTexture, so forces resume from an
interpolated frame; after detecting an active transition
(this.transition.isActive) call this.transition.end(true), then immediately
copy/commit the transition’s target positions into currentPositionTexture (e.g.,
blit or shader pass from the transition’s targetPositionTexture into
currentPositionTexture or invoke a helper like transition.applyFinalPositions)
so the simulation starts from the final positions; apply this exact change at
both places where you currently call this.transition.end(true) (the instance
around the shown diff and the similar block at the other location).

In `@src/modules/Points/index.ts`:
- Around line 2428-2480: The bug is that updateAttributeBuffers currently reuses
the previous GPU buffers (swapping source/target) when counts match, which snaps
the attribute to the old target instead of continuing the in-flight
interpolation; change updateAttributeBuffers (and its callers) to accept the
prior source and prior target CPU Float32Array snapshots and the current
transitionProgress, and when sameCount is true build the next source CPU array
by linearly interpolating each component between priorSource and priorTarget at
transitionProgress (instead of reusing targetBuffer as next source), then create
new GPU buffers (via this.device.createBuffer) from that interpolated source and
the new targetData (or reuse/destroy old buffers appropriately); keep
previousData only for backwards compatibility but prefer priorSource/priorTarget
+ transitionProgress to resume animations smoothly.

---

Outside diff comments:
In `@src/modules/Lines/index.ts`:
- Around line 138-145: The pre-allocated this.colorBuffer and this.widthBuffer
are dead and leak because attributes now use sourceColor/targetColor and
sourceWidth/targetWidth; remove the initial allocations for
colorBuffer/widthBuffer in the constructor/init code, stop aliasing them in
updateColor()/updateWidth(), and update guards in draw() and findHoveredLine()
(and any other checks) to test this.targetColorBuffer and this.targetWidthBuffer
instead of this.colorBuffer/this.widthBuffer; finally remove any remaining
uses/destroys of the old this.colorBuffer/this.widthBuffer fields so only the
target/source buffers are created and destroyed by their existing update/destroy
logic.

---

Duplicate comments:
In `@src/index.ts`:
- Around line 735-745: The pause and abort logic is currently applied for any
pending transition (using this.transition.isPending) which incorrectly affects
color/size/width-only updates; restrict these checks to position-updating
transitions only by adding/using a predicate on Transition (e.g., a method or
flag like affectsPositions or needsPositionTexture) and update both
conditionals: only pause the simulation and call this.config.onSimulationPause
when this.transition.isPending && this.transition.affectsPositions &&
this.store.isSimulationRunning && this.config.transitionDuration > 0 &&
!this._isFirstRenderAfterInit, and only call this.transition.abort when
this.transition.isPending && this.transition.affectsPositions &&
!this.points?.currentPositionTexture so unrelated transitions are not
paused/aborted.

In `@src/modules/Points/index.ts`:
- Around line 2209-2233: The teardown/recreate sequence destroys textures before
their attached framebuffers causing dangling attachments; change the order so
you destroy each framebuffer (e.g., velocityFbo, sourcePositionFbo,
targetPositionFbo, currentPositionFbo, previousPositionFbo) before destroying or
replacing its corresponding texture (velocityTexture, sourcePositionTexture,
targetPositionTexture, currentPositionTexture, previousPositionTexture), and
apply the same ordering in the recreate blocks (the
createTexture/copyImageData/createFramebuffer sequences) found around the
velocity and the other position texture sections.
- Around line 2368-2389: interpolatePosition() updates currentPositionFbo but
doesn't mark cached readback data stale; after the renderPass.end() call in
interpolatePosition (the method named interpolatePosition), invalidate the
tracked positions and cluster centroids caches so subsequent readback APIs won't
return stale data — e.g. call the cache invalidation methods or set the dirty
flags associated with trackedPositionsCache and clusterCentroidsCache (or set
this.trackedPositionsDirty = true / this.clusterCentroidsDirty = true)
immediately after renderPass.end().
- Around line 940-955: The fallback for missing color/size data currently fills
arrays with zeros causing transparent colors and zero-size points; change
updateColor to build the fallback Float32Array from the configured defaults in
this.store (use the actual store property for default point color, e.g. a
default RGBA tuple on this.store or this.store.defaults) instead of new
Float32Array(...).fill(0), and repeat that RGBA per-vertex; likewise update the
analogous size fallback (lines noted 1059-1088) to use the configured default
point size from this.store when data.pointSizes is missing so transition buffers
don’t collapse visuals; keep the existing calls to updateAttributeBuffers and
the same buffer variables (sourceColorBuffer, targetColorBuffer,
previousColorData, colorBuffer).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5e628bce-c6c4-4484-b686-1e92aeb51dde

📥 Commits

Reviewing files that changed from the base of the PR and between d3529ff and 6b540da.

⛔ Files ignored due to path filters (1)
  • src/stories/transition/horsewoman-by-bryullov-1832.jpg is excluded by !**/*.jpg
📒 Files selected for processing (27)
  • .claude/skills/history/SKILL.md
  • CONTRIBUTING.md
  • README.md
  • history/2026/2026-04-22-gpu-transitions.md
  • history/PROMPT.md
  • history/README.md
  • migration-notes.md
  • src/config.ts
  • src/declaration.d.ts
  • src/index.ts
  • src/modules/Drag/index.ts
  • src/modules/GraphData/index.ts
  • src/modules/Lines/draw-curve-line.vert
  • src/modules/Lines/index.ts
  • src/modules/Points/draw-points.vert
  • src/modules/Points/index.ts
  • src/modules/Points/interpolate-position.frag
  • src/modules/Points/position-utils.ts
  • src/modules/Transition/index.ts
  • src/stories/2. configuration.mdx
  • src/stories/3. api-reference.mdx
  • src/stories/transition/point-data.ts
  • src/stories/transition/point-transition.stories.ts
  • src/stories/transition/point-transition.ts
  • src/stories/transition/transition-helpers.ts
  • src/stories/transition/transition.css
  • src/variables.ts
✅ Files skipped from review due to trivial changes (9)
  • src/declaration.d.ts
  • CONTRIBUTING.md
  • history/README.md
  • .claude/skills/history/SKILL.md
  • history/PROMPT.md
  • history/2026/2026-04-22-gpu-transitions.md
  • migration-notes.md
  • src/stories/transition/point-transition.stories.ts
  • src/stories/3. api-reference.mdx
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/stories/transition/transition.css
  • README.md
  • src/modules/Points/interpolate-position.frag
  • src/variables.ts
  • src/stories/2. configuration.mdx
  • src/modules/Lines/draw-curve-line.vert
  • src/modules/Points/draw-points.vert

Comment thread src/index.ts Outdated
Comment thread src/modules/Points/index.ts Outdated
Color/size transitions don't compete with force updates, so they
shouldn't pause the simulation. Add Transition.isPendingFor(property)
and gate the simulation pause on a pending Positions transition only.

Also tidy the point-transition story: drop the color-clustered scatter
reveal (and its helper), snap directly to the picture layout, and move
it under Examples/Transitions. Set transitionDuration: 0 on the
remove-points and moscow-metro-stations stories to avoid unintended
load-time animation.

Signed-off-by: Stukova Olya <stukova.o@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/index.ts (1)

1174-1182: ⚠️ Potential issue | 🟠 Major

Use a position-only transition guard for simulation resume paths.

start() can turn physics back on while a position transition is active, while unpause() and enabling simulation call transition.end(true) for any active transition, including color/size/link-only animations that do not conflict with physics. Gate this on TransitionProperty.Positions consistently.

🐛 Suggested guard sketch
+  private interruptPositionTransitionForSimulation (): void {
+    if (!this.transition.isActiveFor(TransitionProperty.Positions)) return
+    this.transition.end(true)
+  }
+
   public start (alpha = 1): void {
@@
     if (!this.config.enableSimulation) return
     if (!this.graph.pointsNumber) return
     if (this.store.isSimulationRunning) return
+    this.interruptPositionTransitionForSimulation()
@@
-    if (this.transition.isActive) {
-      this.transition.end(true)
-    }
+    this.interruptPositionTransitionForSimulation()
@@
     if (this.config.enableSimulation) {
-      this.transition.end(true)
+      this.interruptPositionTransitionForSimulation()
       this.ensureSimulationModules()

If mixed position + color/link cycles must preserve non-position animations, this should become a scoped Transition API instead of ending the whole active cycle.

Also applies to: 1222-1224, 1532-1534

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 1174 - 1182, The simulation start/resume path
currently calls transition.end(true) and thus ends all active transitions
(color/size/link) even when only positions must resume; update start(),
unpause(), and the code path that enables simulation to only terminate position
transitions by using a position-only guard (e.g., call transition.end(true,
TransitionProperty.Positions) or check the active Transition's properties and
only end if it includes Positions) so non-position animations remain running;
adjust any helper that currently calls transition.end(true) (referenced by
start, unpause, and the enable-simulation branch) to use the Positions-scoped
end call or a check against TransitionProperty.Positions.
🧹 Nitpick comments (1)
src/stories/transition/point-transition.ts (1)

70-83: Cache generated scatter layouts instead of reallocating every tick.

Line 76 regenerates a ~1.6 MB Float32Array for each 200k-point scatter step on every interval. Since LOOP_STEPS only uses fixed tile counts, cache by step to reduce GC pressure during the long-running demo.

♻️ Suggested cache sketch
   const picturePositions = createPicturePositions(cols, rows, spaceSize, aspect)
+  const tilePositionsByStep = new Map<number, Float32Array>()
+  const getTilePositions = (step: number): Float32Array => {
+    let positions = tilePositionsByStep.get(step)
+    if (!positions) {
+      positions = createTileScatterPositions(cols, rows, spaceSize, aspect, step)
+      tilePositionsByStep.set(step, positions)
+    }
+    return positions
+  }
@@
       if (step === undefined) {
         graph.setPointPositions(picturePositions)
       } else {
-        const tilePositions = createTileScatterPositions(
-          cols,
-          rows,
-          spaceSize,
-          aspect,
-          step
-        )
-        graph.setPointPositions(tilePositions)
+        graph.setPointPositions(getTilePositions(step))
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stories/transition/point-transition.ts` around lines 70 - 83, The
interval handler in startLoop regenerates large Float32Array layouts each tick
(via createTileScatterPositions) for the same fixed LOOP_STEPS; introduce a
cache (e.g., Map<number, Float32Array> or Record keyed by step) inside the
module/scope containing startLoop, compute and store tilePositions the first
time createTileScatterPositions(cols, rows, spaceSize, aspect, step) is called
for a given step, and on subsequent ticks reuse the cached array when
loopStepIndex yields that step before calling graph.setPointPositions; also
ensure the cache is invalidated or rebuilt if cols/rows/spaceSize/aspect change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/index.ts`:
- Around line 734-740: The pause logic currently pauses simulation when a
position transition is pending but never resumes it; introduce a boolean flag
(e.g., this._pausedByPositionTransition) and set it to true where the code
currently pauses (the block checking
this.transition.isPendingFor(TransitionProperty.Positions) &&
this.store.isSimulationRunning), then call this.store.isSimulationRunning =
false and this.config.onSimulationPause?.() as before. In the
transition-complete handler (the method that runs when the position transition
ends—referenced near the existing end-of-transition code around where
setPointPositions()/transition completion is handled), check if
this._pausedByPositionTransition is true and if so set
this.store.isSimulationRunning = true, call this.config.onSimulationResume?.()
if applicable, and clear this._pausedByPositionTransition. Also clear
this._pausedByPositionTransition in explicit user pause()/stop() paths so manual
pauses override auto-resume.

---

Outside diff comments:
In `@src/index.ts`:
- Around line 1174-1182: The simulation start/resume path currently calls
transition.end(true) and thus ends all active transitions (color/size/link) even
when only positions must resume; update start(), unpause(), and the code path
that enables simulation to only terminate position transitions by using a
position-only guard (e.g., call transition.end(true,
TransitionProperty.Positions) or check the active Transition's properties and
only end if it includes Positions) so non-position animations remain running;
adjust any helper that currently calls transition.end(true) (referenced by
start, unpause, and the enable-simulation branch) to use the Positions-scoped
end call or a check against TransitionProperty.Positions.

---

Nitpick comments:
In `@src/stories/transition/point-transition.ts`:
- Around line 70-83: The interval handler in startLoop regenerates large
Float32Array layouts each tick (via createTileScatterPositions) for the same
fixed LOOP_STEPS; introduce a cache (e.g., Map<number, Float32Array> or Record
keyed by step) inside the module/scope containing startLoop, compute and store
tilePositions the first time createTileScatterPositions(cols, rows, spaceSize,
aspect, step) is called for a given step, and on subsequent ticks reuse the
cached array when loopStepIndex yields that step before calling
graph.setPointPositions; also ensure the cache is invalidated or rebuilt if
cols/rows/spaceSize/aspect change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b3d3e7ec-992c-472c-99ab-bf3812fd3bf6

📥 Commits

Reviewing files that changed from the base of the PR and between 6b540da and 75b1a15.

📒 Files selected for processing (7)
  • src/index.ts
  • src/modules/Transition/index.ts
  • src/stories/beginners/remove-points/config.ts
  • src/stories/geospatial/moscow-metro-stations/index.ts
  • src/stories/transition/point-transition.stories.ts
  • src/stories/transition/point-transition.ts
  • src/stories/transition/transition-helpers.ts
✅ Files skipped from review due to trivial changes (3)
  • src/stories/geospatial/moscow-metro-stations/index.ts
  • src/stories/beginners/remove-points/config.ts
  • src/stories/transition/point-transition.stories.ts

Comment thread src/index.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/index.ts (2)

1217-1227: ⚠️ Potential issue | 🟠 Major

unpause() starts forces from an interpolated frame, not the target positions.

this.transition.end(true) on Line 1223 only clears Transition state (active set, progress, startTime) — it does not commit the transition's target positions into currentPositionTexture. If a Positions transition is mid-flight when the user unpauses, the force simulation resumes from wherever the interpolated frame happened to be, not from the positions the caller of setPointPositions() requested.

The same concern applies in applyEnableSimulationConfigChange at Line 1533 where transition.end(true) is called right before ensureSimulationModules() / create().

🐛 Suggested helper
+  private finishActivePositionTransitionForSimulation (): void {
+    if (this.transition.isActiveFor(TransitionProperty.Positions)) {
+      this.points?.interpolatePosition(1)
+    }
+    this.transition.end(true)
+  }

Then call finishActivePositionTransitionForSimulation() at both sites instead of transition.end(true).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 1217 - 1227, The unpause() method currently calls
transition.end(true) which only clears Transition state but does not commit the
transition's target positions into currentPositionTexture, causing the
simulation to resume from an interpolated frame; replace the
transition.end(true) call in unpause() with a call to
finishActivePositionTransitionForSimulation() so active Positions transitions
commit their target positions before resuming, and make the same replacement in
applyEnableSimulationConfigChange() right before
ensureSimulationModules()/create(); reference transition.end(true),
finishActivePositionTransitionForSimulation(), unpause(),
applyEnableSimulationConfigChange(), setPointPositions(), currentPositionTexture
and Transition when locating and updating the code.

734-744: ⚠️ Potential issue | 🟠 Major

Simulation paused by a pending position transition is never resumed.

When setPointPositions() is called on a running simulation with transitionDuration > 0, Lines 734–740 pause the simulation for the duration of the position transition, but nothing re-enables it after the transition ends. A graph that was simulating before setPointPositions() stays paused indefinitely once the animation completes.

Track the "paused-for-transition" state (e.g. a shouldResumeSimulationAfterPositionTransition flag set here and cleared in renderFrame once transition.isActiveFor(TransitionProperty.Positions) flips from true → false), then flip isSimulationRunning back on and fire onSimulationUnpause?.(). Also clear the flag in explicit pause()/stop()/destroy() paths so manual user actions win over auto-resume.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 734 - 744, The code pauses the running simulation
when a positions transition starts but never resumes it; add a boolean flag
(e.g. shouldResumeSimulationAfterPositionTransition) and set it where you
currently set store.isSimulationRunning = false (inside the block that checks
transition.isPendingFor(TransitionProperty.Positions), config.transitionDuration
> 0, and !_isFirstRenderAfterInit), then in renderFrame detect when
transition.isActiveFor(TransitionProperty.Positions) flips from true to false
and if the flag is set re-enable store.isSimulationRunning, call
config.onSimulationUnpause?.(), and clear the flag; also ensure the flag is
cleared whenever pause(), stop(), or destroy() are invoked so manual actions
override auto-resume; leave the existing transition.abort logic
(transition.isPending && !points?.currentPositionTexture) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/index.ts`:
- Around line 1549-1556: The onSimulationEnd callback is being invoked
unconditionally when disabling simulation; mirror the logic used in stop() to
avoid spurious end events: before mutating
this.store.isSimulationRunning/alpha/simulationProgress, compute a
wasSimulationActive boolean (e.g. const wasSimulationActive =
this.store.isSimulationRunning || this.store.alpha > 0 ||
this.store.simulationProgress > 0) and only call this.config.onSimulationEnd?.()
if wasSimulationActive is true; leave the rest of the cleanup
(this._shouldForceHoverDetection = true and this.destroySimulationModules())
unchanged.

---

Duplicate comments:
In `@src/index.ts`:
- Around line 1217-1227: The unpause() method currently calls
transition.end(true) which only clears Transition state but does not commit the
transition's target positions into currentPositionTexture, causing the
simulation to resume from an interpolated frame; replace the
transition.end(true) call in unpause() with a call to
finishActivePositionTransitionForSimulation() so active Positions transitions
commit their target positions before resuming, and make the same replacement in
applyEnableSimulationConfigChange() right before
ensureSimulationModules()/create(); reference transition.end(true),
finishActivePositionTransitionForSimulation(), unpause(),
applyEnableSimulationConfigChange(), setPointPositions(), currentPositionTexture
and Transition when locating and updating the code.
- Around line 734-744: The code pauses the running simulation when a positions
transition starts but never resumes it; add a boolean flag (e.g.
shouldResumeSimulationAfterPositionTransition) and set it where you currently
set store.isSimulationRunning = false (inside the block that checks
transition.isPendingFor(TransitionProperty.Positions), config.transitionDuration
> 0, and !_isFirstRenderAfterInit), then in renderFrame detect when
transition.isActiveFor(TransitionProperty.Positions) flips from true to false
and if the flag is set re-enable store.isSimulationRunning, call
config.onSimulationUnpause?.(), and clear the flag; also ensure the flag is
cleared whenever pause(), stop(), or destroy() are invoked so manual actions
override auto-resume; leave the existing transition.abort logic
(transition.isPending && !points?.currentPositionTexture) unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 427e7fc7-6af9-4af2-a157-d7793e45b2ad

📥 Commits

Reviewing files that changed from the base of the PR and between 6b540da and 75b1a15.

📒 Files selected for processing (7)
  • src/index.ts
  • src/modules/Transition/index.ts
  • src/stories/beginners/remove-points/config.ts
  • src/stories/geospatial/moscow-metro-stations/index.ts
  • src/stories/transition/point-transition.stories.ts
  • src/stories/transition/point-transition.ts
  • src/stories/transition/transition-helpers.ts
✅ Files skipped from review due to trivial changes (1)
  • src/stories/geospatial/moscow-metro-stations/index.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/stories/beginners/remove-points/config.ts
  • src/stories/transition/point-transition.stories.ts
  • src/stories/transition/transition-helpers.ts
  • src/stories/transition/point-transition.ts

Comment thread src/index.ts
…ases

- Invalidate position/centroid caches after interpolatePosition so
  trackedPositions and centroid getters don't return stale data during
  a position transition with simulation inactive
- Skip hover detection while transitions are active (hover shaders read
  target buffers and would mismatch the visible interpolated geometry)
- Guard onSimulationEnd in applyEnableSimulationConfigChange against
  spurious firing when disabling a never-running simulation
- Drop dead colorBuffer/sizeBuffer aliases in Points and colorBuffer/
  widthBuffer in Lines; Lines init allocations were leaking on first update
- Match Framebuffer→Texture destroy order in resize branches with destroy()
- Replace zero-fill fallbacks in Points updateColor/updateSize with
  \`as Float32Array\` + invariant comment
- Document that setPointPositions intentionally keeps simulation paused

Signed-off-by: Stukova Olya <stukova.o@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/modules/Points/index.ts (1)

2483-2528: ⚠️ Potential issue | 🟠 Major

Destroy order in createOrUpdatePositionTextures violates the documented Framebuffer→Texture pattern.

The other recreate paths in this PR (ensureSimulationResources, createTransitionResources, destroyTransitionResources, destroySimulationResources, and destroy()) all follow the documented order "Models → Framebuffers → Textures → UniformStores → Buffers". This method still destroys the texture before the framebuffer that holds it as a color attachment, leaving the FBO momentarily attached to a destroyed texture. Mirror the same fix here for both currentPosition* and previousPosition*.

🛠️ Proposed ordering fix
     if (!this.currentPositionTexture || this.currentPositionTexture.width !== pointsTextureSize || this.currentPositionTexture.height !== pointsTextureSize) {
-      if (this.currentPositionTexture && !this.currentPositionTexture.destroyed) {
-        this.currentPositionTexture.destroy()
-      }
       if (this.currentPositionFbo && !this.currentPositionFbo.destroyed) {
         this.currentPositionFbo.destroy()
       }
+      if (this.currentPositionTexture && !this.currentPositionTexture.destroyed) {
+        this.currentPositionTexture.destroy()
+      }
       this.currentPositionTexture = this.device.createTexture({
     if (!this.previousPositionTexture ||
         this.previousPositionTexture.width !== pointsTextureSize ||
         this.previousPositionTexture.height !== pointsTextureSize) {
-      if (this.previousPositionTexture && !this.previousPositionTexture.destroyed) {
-        this.previousPositionTexture.destroy()
-      }
       if (this.previousPositionFbo && !this.previousPositionFbo.destroyed) {
         this.previousPositionFbo.destroy()
       }
+      if (this.previousPositionTexture && !this.previousPositionTexture.destroyed) {
+        this.previousPositionTexture.destroy()
+      }
       this.previousPositionTexture = this.device.createTexture({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/Points/index.ts` around lines 2483 - 2528, In
createOrUpdatePositionTextures, the destroy order currently destroys
currentPositionTexture (and previousPositionTexture) before their associated
framebuffers, violating the Framebuffer→Texture pattern; update both blocks so
you first destroy this.currentPositionFbo (if exists and not destroyed) then
destroy this.currentPositionTexture, and likewise destroy
this.previousPositionFbo before this.previousPositionTexture, keeping the same
existence checks (e.g., this.currentPositionFbo &&
!this.currentPositionFbo.destroyed) and preserving the subsequent
createTexture/createFramebuffer calls and copyImageData usage.
🧹 Nitpick comments (1)
src/index.ts (1)

256-256: Ad-hoc field assignment couples Points to Transition after construction.

Setting this.points.transition = this.transition outside the Points constructor relies on Points exposing a public mutable transition field; it also leaves a brief window where a freshly-constructed Points has no transition reference (anything that reads it during the same tick would crash/no-op). Threading transition through the Points constructor (like Drag already does) would make the dependency explicit and remove the ordering trap.

♻️ Sketch
-      this.points = new Points(device, this.config, this.store, this.graph)
-      this.points.transition = this.transition
+      this.points = new Points(device, this.config, this.store, this.graph, this.transition)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` at line 256, The Points instance is being coupled to Transition
after construction via the ad-hoc assignment this.points.transition =
this.transition; instead pass the Transition into the Points constructor
(mirroring how Drag is constructed) so Points receives and stores the reference
during construction, remove the external assignment, and update the Points class
signature (constructor) and all call sites where new Points(...) is created to
accept and store the Transition dependency (ensure any public API used elsewhere
is preserved or exposed via constructor-set fields/methods).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/index.ts`:
- Around line 380-382: The guard that queues position transitions only checks
this.points?.currentPositionTexture for truthiness but must also ensure the
texture isn't destroyed; update both the pre-queue check (around the current
transition.queue(TransitionProperty.Positions) call) and the abort() fallback
guard to verify the texture is not destroyed (e.g., ensure
this.points.currentPositionTexture &&
!this.points.currentPositionTexture.destroyed) before calling transition.queue,
transition.start, or points.interpolatePosition so no operations run on a
destroyed GPU resource.

In `@src/modules/Lines/index.ts`:
- Around line 1066-1118: updateAttributeBuffers is reusing buffers in the
sameCount fast path (setting nextSource = targetBuffer) which causes a visual
snap because it doesn't capture the currently interpolated frame; instead, when
queuing a new transition mid-flight capture the actual interpolated attribute
values and seed sourceData with them before swapping buffers. Modify
updateAttributeBuffers (and the fast-path branch that uses
nextSource/nextTarget) to either read back the GPU-generated interpolated values
(e.g., via the same read-back used in Points) or compute CPU-side interpolation
from previousData and targetData using the current transitionProgress, then
write those interpolated values into the buffer you will use as the new source
(rather than using raw targetBuffer contents), ensuring source/previous reflect
the visible frame so the next transition interpolates smoothly.

---

Duplicate comments:
In `@src/modules/Points/index.ts`:
- Around line 2483-2528: In createOrUpdatePositionTextures, the destroy order
currently destroys currentPositionTexture (and previousPositionTexture) before
their associated framebuffers, violating the Framebuffer→Texture pattern; update
both blocks so you first destroy this.currentPositionFbo (if exists and not
destroyed) then destroy this.currentPositionTexture, and likewise destroy
this.previousPositionFbo before this.previousPositionTexture, keeping the same
existence checks (e.g., this.currentPositionFbo &&
!this.currentPositionFbo.destroyed) and preserving the subsequent
createTexture/createFramebuffer calls and copyImageData usage.

---

Nitpick comments:
In `@src/index.ts`:
- Line 256: The Points instance is being coupled to Transition after
construction via the ad-hoc assignment this.points.transition = this.transition;
instead pass the Transition into the Points constructor (mirroring how Drag is
constructed) so Points receives and stores the reference during construction,
remove the external assignment, and update the Points class signature
(constructor) and all call sites where new Points(...) is created to accept and
store the Transition dependency (ensure any public API used elsewhere is
preserved or exposed via constructor-set fields/methods).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f66eaf3a-890c-4bcc-bfc6-50a03c5f3eb9

📥 Commits

Reviewing files that changed from the base of the PR and between 75b1a15 and 74e0567.

📒 Files selected for processing (4)
  • src/config.ts
  • src/index.ts
  • src/modules/Lines/index.ts
  • src/modules/Points/index.ts

Comment thread src/index.ts Outdated
Comment thread src/modules/Lines/index.ts Outdated
- Update 2026-04-22-gpu-transitions.md to reflect post-merge state
  and follow-up fixes (c5fd30e, 75b1a15, 74e0567): auto-pause is now
  position-only, hover skipped during transitions, position/centroid
  caches invalidated, behavior matrix split into position vs
  color/size rows, Storybook path fixed
- Add --update mode to the /history skill so follow-up commits
  extending an existing topic revise the latest entry in place
  without re-asking for a "why"
- Mention /history --update in CONTRIBUTING.md and history/README.md

Signed-off-by: Stukova Olya <stukova.o@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@history/2026/2026-04-22-gpu-transitions.md`:
- Line 68: Update the sentence that ends with “and stay paused afterwards” to
use US spelling by replacing “afterwards” with “afterward”; locate the line
mentioning the new `transitionDuration` default and the sentence that reads
"Call `graph.unpause()` to resume forces." and change it to "...and stay paused
afterward. Call `graph.unpause()` to resume forces."
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c599ee9c-12ee-41e2-8164-14b774ed285a

📥 Commits

Reviewing files that changed from the base of the PR and between 74e0567 and c9d3f6a.

📒 Files selected for processing (4)
  • .claude/skills/history/SKILL.md
  • CONTRIBUTING.md
  • history/2026/2026-04-22-gpu-transitions.md
  • history/README.md
✅ Files skipped from review due to trivial changes (3)
  • CONTRIBUTING.md
  • .claude/skills/history/SKILL.md
  • history/README.md

Comment thread history/2026/2026-04-22-gpu-transitions.md
- src/index.ts: skip pending position transition queue/abort when currentPositionTexture is destroyed.
- src/modules/{Lines,Points}/index.ts: add TODO for rare mid-animation attribute-update smoothing edge case.

Signed-off-by: Stukova Olya <stukova.o@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
src/modules/Points/index.ts (2)

2484-2530: ⚠️ Potential issue | 🟡 Minor

Destroy the FBO before its attached texture in recreate paths.

createOrUpdatePositionTextures() still tears down currentPositionTexture / previousPositionTexture before the framebuffers that reference them. The rest of this module already follows Framebuffers -> Textures; this branch should do the same.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/Points/index.ts` around lines 2484 - 2530, In
createOrUpdatePositionTextures(), the teardown destroys currentPositionTexture /
previousPositionTexture before their framebuffers; reverse the order so you
destroy currentPositionFbo before currentPositionTexture and previousPositionFbo
before previousPositionTexture (check and respect the destroyed checks),
ensuring you call destroy() on the framebuffer objects (currentPositionFbo and
previousPositionFbo) prior to destroying their attached textures
(currentPositionTexture and previousPositionTexture) and keep the existing
recreate logic that creates the texture then the framebuffer.

2438-2450: ⚠️ Potential issue | 🟠 Major

Chain point attribute transitions from the interpolated value.

The same-count fast path still reuses the previous target buffer as the next source. A second setPointColors() or setPointSizes() during an active transition will visibly snap before animating again.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/Points/index.ts` around lines 2438 - 2450, The fast-path is
reusing the prior target buffer as the next source so a new
setPointColors()/setPointSizes() during an active transition snaps because
previous is set to the raw new targetData; instead capture the current
interpolated values from the buffer that will become the source (targetBuffer /
nextSource) and use those as the previous array. Update the branch where
nextSource = targetBuffer and nextTarget = sourceBuffer so that you read/copy
the current contents of nextSource (the buffer being reused as the new source)
into previous (e.g., clone its current attribute data) and keep
nextTarget.write(targetData) for the new target; reference sameCount,
sourceBuffer, targetBuffer, nextSource, nextTarget, and previous to locate the
change.
src/modules/Lines/index.ts (1)

1077-1089: ⚠️ Potential issue | 🟠 Major

Chain link attribute transitions from the displayed frame.

This fast path still promotes the old target buffer to the next source. If setLinkColors() or setLinkWidths() lands mid-transition, the next animation snaps to the previous target before continuing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/Lines/index.ts` around lines 1077 - 1089, The fast-path promotes
the old target buffer to the next source which causes new
setLinkColors()/setLinkWidths() calls mid-transition to snap to the previous
target; fix the swap in the sameCount branch by ensuring the returned previous
array reflects the currently displayed attributes (the buffer being shown, i.e.,
nextSource) instead of blindly using targetData—when you set nextSource =
targetBuffer and nextTarget = sourceBuffer, build previous from the nextSource's
current attribute contents (read or clone its backing Float32Array) so
subsequent transitions chain smoothly from the displayed frame.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modules/Points/index.ts`:
- Around line 268-271: The shouldAnimate gate currently uses
this.transition?.isPending and can be triggered by unrelated queued transitions;
narrow it to only pending Positions transitions by checking this.transition is
pending AND is for Positions (e.g., inspect this.transition.type or
this.transition.key === 'Positions') and also ensure the texture is alive (check
this.currentPositionTexture exists and is not destroyed) before setting
shouldAnimate so updatePositions() only follows the position-transition path;
update the shouldAnimate expression accordingly (referencing shouldAnimate,
this.transition, this.config.transitionDuration, this.currentPositionTexture,
and updatePositions()).

---

Duplicate comments:
In `@src/modules/Lines/index.ts`:
- Around line 1077-1089: The fast-path promotes the old target buffer to the
next source which causes new setLinkColors()/setLinkWidths() calls
mid-transition to snap to the previous target; fix the swap in the sameCount
branch by ensuring the returned previous array reflects the currently displayed
attributes (the buffer being shown, i.e., nextSource) instead of blindly using
targetData—when you set nextSource = targetBuffer and nextTarget = sourceBuffer,
build previous from the nextSource's current attribute contents (read or clone
its backing Float32Array) so subsequent transitions chain smoothly from the
displayed frame.

In `@src/modules/Points/index.ts`:
- Around line 2484-2530: In createOrUpdatePositionTextures(), the teardown
destroys currentPositionTexture / previousPositionTexture before their
framebuffers; reverse the order so you destroy currentPositionFbo before
currentPositionTexture and previousPositionFbo before previousPositionTexture
(check and respect the destroyed checks), ensuring you call destroy() on the
framebuffer objects (currentPositionFbo and previousPositionFbo) prior to
destroying their attached textures (currentPositionTexture and
previousPositionTexture) and keep the existing recreate logic that creates the
texture then the framebuffer.
- Around line 2438-2450: The fast-path is reusing the prior target buffer as the
next source so a new setPointColors()/setPointSizes() during an active
transition snaps because previous is set to the raw new targetData; instead
capture the current interpolated values from the buffer that will become the
source (targetBuffer / nextSource) and use those as the previous array. Update
the branch where nextSource = targetBuffer and nextTarget = sourceBuffer so that
you read/copy the current contents of nextSource (the buffer being reused as the
new source) into previous (e.g., clone its current attribute data) and keep
nextTarget.write(targetData) for the new target; reference sameCount,
sourceBuffer, targetBuffer, nextSource, nextTarget, and previous to locate the
change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ea85d3ed-122b-4a0e-af9f-85ebb8f2cb31

📥 Commits

Reviewing files that changed from the base of the PR and between c9d3f6a and b775c4c.

📒 Files selected for processing (3)
  • src/index.ts
  • src/modules/Lines/index.ts
  • src/modules/Points/index.ts

Comment thread src/modules/Points/index.ts
Signed-off-by: Stukova Olya <stukova.o@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/modules/Points/index.ts (2)

1449-1473: ⚠️ Potential issue | 🟡 Minor

Animate hover/focus ring size with the point.

The ring pass still sizes from data.pointSizes / data.pointImageSizes, so the outline jumps to the final radius while the underlying point is still interpolating. Use the same source/target size pair plus transitionProgress here when shouldAnimatePointSizes is active.

Also applies to: 1483-1507

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/Points/index.ts` around lines 1449 - 1473, The ring sizing uses
data.pointSizes/data.pointImageSizes directly causing a jump; update the logic
inside drawHighlightedUniformStore.setUniforms (the block that reads pointSize =
data.pointSizes?.[store.hoveredPoint.index] ...) to check
shouldAnimatePointSizes and the hovered point's transitionProgress (e.g.,
hoveredPoint.transitionProgress or similar fields used elsewhere) and compute an
interpolated size from the transition's source/target size pair instead of the
static arrays; use that interpolated value for both pointSize and imageSize (and
for the other similar block around lines 1483-1507) so the ring animates in sync
with the point.

1063-1119: ⚠️ Potential issue | 🟠 Major

Keep hit-testing and selection on the same size curve as rendering.

updateSize() now publishes only the final size state (targetSizeBuffer / sizeTexture). The draw shader interpolates point size, but hover picking and rect/polygon queries consume the snapped target immediately, so points can be hovered or selected outside their visible radius during a size transition.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/Points/index.ts` around lines 1063 - 1119, updateSize() currently
writes the snapped final sizes into sizeTexture (used by hit-testing) from
data.pointSizes, causing hover/selection to use the target size during
transitions; change it to use the currently uploaded source/previous buffer
values for the sizeTexture so hit-testing matches the visible interpolated size.
Specifically, after calling updateAttributeBuffers(...) use the returned source
(or previous if source is not present) buffer/content instead of data.pointSizes
to populate initialState (i.e., derive per-point size from the source/previous
buffer values you just got), and then copy that initialState into sizeTexture
(and when updating sizeTexture on subsequent frames) so sizeTexture reflects the
current visible size rather than the snapped target; keep drawCommand attribute
updates for sourceSize/targetSize as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/modules/Points/index.ts`:
- Around line 1449-1473: The ring sizing uses
data.pointSizes/data.pointImageSizes directly causing a jump; update the logic
inside drawHighlightedUniformStore.setUniforms (the block that reads pointSize =
data.pointSizes?.[store.hoveredPoint.index] ...) to check
shouldAnimatePointSizes and the hovered point's transitionProgress (e.g.,
hoveredPoint.transitionProgress or similar fields used elsewhere) and compute an
interpolated size from the transition's source/target size pair instead of the
static arrays; use that interpolated value for both pointSize and imageSize (and
for the other similar block around lines 1483-1507) so the ring animates in sync
with the point.
- Around line 1063-1119: updateSize() currently writes the snapped final sizes
into sizeTexture (used by hit-testing) from data.pointSizes, causing
hover/selection to use the target size during transitions; change it to use the
currently uploaded source/previous buffer values for the sizeTexture so
hit-testing matches the visible interpolated size. Specifically, after calling
updateAttributeBuffers(...) use the returned source (or previous if source is
not present) buffer/content instead of data.pointSizes to populate initialState
(i.e., derive per-point size from the source/previous buffer values you just
got), and then copy that initialState into sizeTexture (and when updating
sizeTexture on subsequent frames) so sizeTexture reflects the current visible
size rather than the snapped target; keep drawCommand attribute updates for
sourceSize/targetSize as-is.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ea19cd6d-9931-4401-b11d-441f703ca231

📥 Commits

Reviewing files that changed from the base of the PR and between b775c4c and 45a12f4.

📒 Files selected for processing (1)
  • src/modules/Points/index.ts

Signed-off-by: Stukova Olya <stukova.o@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
history/2026/2026-04-22-gpu-transitions.md (1)

56-64: Clarify matrix wording to explicitly include link transitions in the non-pausing path.

The current labels say “color/size transition,” but this PR also transitions link color/width. Expanding wording avoids ambiguity for readers.

✏️ Suggested doc tweak
-Each row is the state when `render()` fires. The last two columns show what happens if you flip each config via `setConfigPartial` from that state. Auto-pause applies to **position** transitions only — color/size transitions never pause the simulation.
+Each row is the state when `render()` fires. The last two columns show what happens if you flip each config via `setConfigPartial` from that state. Auto-pause applies to **position** transitions only — point/link attribute transitions (point color/size, link color/width) never pause the simulation.

-| `true` + `>0` (simulation, **color/size** transition) | Animate. Simulation keeps running — color/size transitions don't compete with forces. | `→ false`: stops simulation, destroys simulation-only resources, fires `onSimulationEnd`. | `→ ≤0`: next `start()` snaps. A running transition ends with `onTransitionEnd(true)` on the next step. |
+| `true` + `>0` (simulation, **attribute** transition: point color/size or link color/width) | Animate. Simulation keeps running — attribute transitions don't compete with forces. | `→ false`: stops simulation, destroys simulation-only resources, fires `onSimulationEnd`. | `→ ≤0`: next `start()` snaps. A running transition ends with `onTransitionEnd(true)` on the next step. |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@history/2026/2026-04-22-gpu-transitions.md` around lines 56 - 64, The wording
in the transition matrix is ambiguous about which transitions do not auto-pause
the simulation; update the "color/size transition" entries to explicitly include
link transitions so readers know link color/width changes don't pause
simulation. Edit the table rows that currently read "color/size transition" to
say something like "non-positional transitions (color, size, link color/width)"
and keep references to render(), setConfigPartial, enableSimulation,
transitionDuration, onSimulationPause, unpause(), and onTransitionEnd(true)
intact so the behavior description and signals remain precise.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@history/2026/2026-04-22-gpu-transitions.md`:
- Around line 56-64: The wording in the transition matrix is ambiguous about
which transitions do not auto-pause the simulation; update the "color/size
transition" entries to explicitly include link transitions so readers know link
color/width changes don't pause simulation. Edit the table rows that currently
read "color/size transition" to say something like "non-positional transitions
(color, size, link color/width)" and keep references to render(),
setConfigPartial, enableSimulation, transitionDuration, onSimulationPause,
unpause(), and onTransitionEnd(true) intact so the behavior description and
signals remain precise.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 859acddc-8377-462b-b7c0-7d1db727bff8

📥 Commits

Reviewing files that changed from the base of the PR and between 45a12f4 and a826398.

📒 Files selected for processing (3)
  • .agents/skills/history/SKILL.md
  • CONTRIBUTING.md
  • history/2026/2026-04-22-gpu-transitions.md
✅ Files skipped from review due to trivial changes (2)
  • .agents/skills/history/SKILL.md
  • CONTRIBUTING.md

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@history/2026/2026-04-22-gpu-transitions.md`:
- Around line 61-64: Replace the misleading phrase "next `start()` snaps" with
render-cycle wording: explain that changing duration to ≤0 causes the next
update + render cycle to snap immediately rather than `start()` triggering the
snap, and note that `start()` only changes simulation state (see start() and
render() in src/index.ts); keep references to transition lifecycle callbacks
like onTransitionEnd(true) and onSimulationEnd so integrators understand when
the snap/cleanup occurs relative to those events.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 670f16c6-48b7-4229-a36f-be1929a885dc

📥 Commits

Reviewing files that changed from the base of the PR and between 45a12f4 and a826398.

📒 Files selected for processing (3)
  • .agents/skills/history/SKILL.md
  • CONTRIBUTING.md
  • history/2026/2026-04-22-gpu-transitions.md
✅ Files skipped from review due to trivial changes (2)
  • CONTRIBUTING.md
  • .agents/skills/history/SKILL.md

Comment thread history/2026/2026-04-22-gpu-transitions.md Outdated
…isely

Signed-off-by: Stukova Olya <stukova.o@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
history/2026/2026-04-22-gpu-transitions.md (1)

30-30: Broaden “color/size” wording to explicitly include link transitions.

Line 30 and Line 64 currently read as if only point color/size transitions are non-pausing. Since this PR also transitions link color/width, a small wording tweak would prevent ambiguity.

✏️ Suggested wording update
-**Auto-pause (position transitions only).** When `render()` sees a pending **position** transition with `transitionDuration > 0` and a running simulation (and it's not the first render), the simulation pauses before the transition starts and `onSimulationPause` fires. The simulation **stays paused after the transition ends** — `setPointPositions()` signals the user wants to explore a specific layout, not have forces immediately pull nodes away from it. Call `unpause()` to resume explicitly. Color and size transitions don't compete with force updates and never pause the simulation.
+**Auto-pause (position transitions only).** When `render()` sees a pending **position** transition with `transitionDuration > 0` and a running simulation (and it's not the first render), the simulation pauses before the transition starts and `onSimulationPause` fires. The simulation **stays paused after the transition ends** — `setPointPositions()` signals the user wants to explore a specific layout, not have forces immediately pull nodes away from it. Call `unpause()` to resume explicitly. Point color/size and link color/width transitions don't compete with force updates and never pause the simulation.
-| `true` + `>0` (simulation, **color/size** transition) | Animate. Simulation keeps running — color/size transitions don't compete with forces. | `→ false`: stops simulation, destroys simulation-only resources, fires `onSimulationEnd`. | `→ ≤0`: the next update + render cycle snaps immediately instead of animating. `start()` only changes simulation state. If a transition is already running, the next render frame interrupts it with `onTransitionEnd(true)`; `onSimulationEnd` still only fires if the simulation is later stopped or finishes. |
+| `true` + `>0` (simulation, **color/size/link color/width** transition) | Animate. Simulation keeps running — these transitions don't compete with forces. | `→ false`: stops simulation, destroys simulation-only resources, fires `onSimulationEnd`. | `→ ≤0`: the next update + render cycle snaps immediately instead of animating. `start()` only changes simulation state. If a transition is already running, the next render frame interrupts it with `onTransitionEnd(true)`; `onSimulationEnd` still only fires if the simulation is later stopped or finishes. |

Also applies to: 64-64

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@history/2026/2026-04-22-gpu-transitions.md` at line 30, The paragraph around
render(), transitionDuration, setPointPositions(), onSimulationPause and unpause
should be reworded to explicitly state that non-pausing transitions include
point color/size and link color/width (or link color/width/size as applicable)
so readers don’t assume only point visuals are exempt; update the sentence that
currently reads “Color and size transitions don't compete with force updates and
never pause the simulation” to something like “Color and size transitions (and
link color/width transitions) don't compete with force updates and never pause
the simulation,” and apply the same wording change at the other occurrence
referenced (line ~64).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@history/2026/2026-04-22-gpu-transitions.md`:
- Line 30: The paragraph around render(), transitionDuration,
setPointPositions(), onSimulationPause and unpause should be reworded to
explicitly state that non-pausing transitions include point color/size and link
color/width (or link color/width/size as applicable) so readers don’t assume
only point visuals are exempt; update the sentence that currently reads “Color
and size transitions don't compete with force updates and never pause the
simulation” to something like “Color and size transitions (and link color/width
transitions) don't compete with force updates and never pause the simulation,”
and apply the same wording change at the other occurrence referenced (line ~64).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e344e4b9-1c43-473c-a669-ee6d3482b33d

📥 Commits

Reviewing files that changed from the base of the PR and between a826398 and bcbbdcb.

📒 Files selected for processing (1)
  • history/2026/2026-04-22-gpu-transitions.md

@Stukova Stukova requested a review from rokotyan April 28, 2026 15:15
Bring back repeated start(alpha) calls as a reheat mechanism while
keeping onSimulationStart limited to stopped/paused -> running.

Signed-off-by: Stukova Olya <stukova.o@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/index.ts (1)

740-750: ⚠️ Potential issue | 🟠 Major

Drop only the invalid position transition before auto-pausing.

Line 750 calls abort(), which clears the entire pending queue and any active cycle. If the invalid piece is only TransitionProperty.Positions, this also discards unrelated point/link color or size/width transitions. Because Lines 740-745 pause first, it can also leave the simulation paused even though no transition survives.

🔧 Narrow the fallback to positions and validate before pausing
-    if (this.transition.isPendingFor(TransitionProperty.Positions) &&
-        this.store.isSimulationRunning &&
-        this.config.transitionDuration > 0 &&
-        !this._isFirstRenderAfterInit) {
-      this.store.isSimulationRunning = false
-      this.config.onSimulationPause?.()
-    }
-
     const currentPositionTexture = this.points?.currentPositionTexture
-    if (this.transition.isPending && (!currentPositionTexture || currentPositionTexture.destroyed)) {
-      this.transition.abort()
+    const hasValidPositionTexture = !!currentPositionTexture && !currentPositionTexture.destroyed
+    if (this.transition.isPendingFor(TransitionProperty.Positions) && !hasValidPositionTexture) {
+      this.transition.dequeue?.(TransitionProperty.Positions)
+    }
+
+    if (this.transition.isPendingFor(TransitionProperty.Positions) &&
+        hasValidPositionTexture &&
+        this.store.isSimulationRunning &&
+        this.config.transitionDuration > 0 &&
+        !this._isFirstRenderAfterInit) {
+      this.store.isSimulationRunning = false
+      this.config.onSimulationPause?.()
     }

Based on learnings, the pause itself is intentional for valid position transitions; the issue here is the broad abort/ordering, not the lack of auto-resume.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 740 - 750, The code currently calls
this.transition.abort() which clears all pending and active transitions
(including non-position ones) and can leave the simulation paused incorrectly;
instead drop only invalid position transitions and validate before pausing:
locate the checks using
this.transition.isPendingFor(TransitionProperty.Positions) and
currentPositionTexture, replace the global this.transition.abort() call with a
targeted removal of only pending position transitions (e.g., a method like
dropPendingFor or abortPendingFor with TransitionProperty.Positions), then
re-check that a position transition still exists before setting
this.store.isSimulationRunning = false and calling
this.config.onSimulationPause?.().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/index.ts`:
- Around line 2017-2020: The current hover gate unconditionally returns when
this.transition.isActive, which also blocks hover during color-only transitions;
change the condition to only block when the active transition actually affects
geometry (e.g. points/links sizes/positions). Replace the unconditional check
using this.transition.isActive with a predicate that checks geometry-affecting
transitions (for example: if (this.transition.isActive &&
this.transition.affectsGeometry) return), or if your Transition type uses a
kind/flags field use that (e.g. this.transition.kind === 'geometry' or
this.transition.hasFlag('geometry')). If such an indicator doesn't exist add a
boolean like affectsGeometry to the Transition class/instance and set it when
starting transitions that change vertex geometry so hover/cursor updates remain
enabled for color-only transitions.

---

Duplicate comments:
In `@src/index.ts`:
- Around line 740-750: The code currently calls this.transition.abort() which
clears all pending and active transitions (including non-position ones) and can
leave the simulation paused incorrectly; instead drop only invalid position
transitions and validate before pausing: locate the checks using
this.transition.isPendingFor(TransitionProperty.Positions) and
currentPositionTexture, replace the global this.transition.abort() call with a
targeted removal of only pending position transitions (e.g., a method like
dropPendingFor or abortPendingFor with TransitionProperty.Positions), then
re-check that a position transition still exists before setting
this.store.isSimulationRunning = false and calling
this.config.onSimulationPause?.().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3dfff0bf-9c95-434e-9a0e-e48d4f118930

📥 Commits

Reviewing files that changed from the base of the PR and between bcbbdcb and 579801d.

📒 Files selected for processing (2)
  • src/index.ts
  • src/stories/3. api-reference.mdx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/stories/3. api-reference.mdx

Comment thread src/index.ts Outdated
Only block hover for point size transitions.

Block drag start for position and point size transitions, and update
docs to match the current behavior.

Signed-off-by: Stukova Olya <stukova.o@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/index.ts`:
- Around line 1543-1560: When enabling simulation (the block that calls
this.transition.end(true)), also remove any queued "Positions" transitions that
were enqueued by setPointPositions so they don't resume on the next render;
locate the Transition instance (this.transition) and either call its specific
dequeue/abort API for position-type entries (e.g., abort('positions') or
abortPendingOfType('Positions')) or filter its pending queue (e.g., remove
entries whose type/command equals Positions) so only non-position pending
transitions survive; this prevents a queued Positions transition from running
after setConfigPartial({ enableSimulation: true }) and causing an immediate
auto-pause in render().
- Around line 1184-1190: Before re-enabling physics in start(), ensure any
active position transitions are finished to avoid GPU interpolation overwriting
simulation steps: before setting this.store.isSimulationRunning = true (i.e., at
the top of the shown start() snippet), check for and end active position
transitions for TransitionProperty.Positions—mirror the unpause() guard by
calling the same transition termination path (e.g.,
this.transitions.end(TransitionProperty.Positions) or the existing isActive/stop
helper used elsewhere) so the force step runs against final simulation
coordinates.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ed07619a-6e54-460d-83fb-b1d1fe989bdb

📥 Commits

Reviewing files that changed from the base of the PR and between 579801d and 5006eea.

📒 Files selected for processing (5)
  • history/2026/2026-04-22-gpu-transitions.md
  • src/index.ts
  • src/modules/Drag/index.ts
  • src/stories/2. configuration.mdx
  • src/stories/3. api-reference.mdx
✅ Files skipped from review due to trivial changes (1)
  • src/stories/2. configuration.mdx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/modules/Drag/index.ts

Comment thread src/index.ts
Comment thread src/index.ts
  start() and unpause() now end transitions only when positions are animating.
  Enabling simulation also drops any queued position transition to prevent
  immediate auto-pause on next render().

Signed-off-by: Stukova Olya <stukova.o@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modules/Transition/index.ts`:
- Around line 155-156: The progress calculation can go negative when nowMs <
this.startTime, causing invalid inputs to this.applyEasing; change the
computation of linear in Transition to clamp the lower bound to 0 (e.g., use
Math.max or a clamp) so linear = clamp((nowMs - this.startTime) /
transitionDuration, 0, 1) before calling this.applyEasing; update the expression
near the existing linear/ eased lines to ensure transitionDuration is used and
avoid division by zero if needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bc9546c7-d67c-431f-bbe5-a504b989bd32

📥 Commits

Reviewing files that changed from the base of the PR and between 5006eea and fed8dcd.

📒 Files selected for processing (2)
  • src/index.ts
  • src/modules/Transition/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/index.ts

Comment thread src/modules/Transition/index.ts Outdated
Stukova added 4 commits April 29, 2026 15:09
Signed-off-by: Stukova Olya <stukova.o@gmail.com>
Signed-off-by: Stukova Olya <stukova.o@gmail.com>
Signed-off-by: Stukova Olya <stukova.o@gmail.com>
Signed-off-by: Stukova Olya <stukova.o@gmail.com>
@Stukova
Copy link
Copy Markdown
Member Author

Stukova commented Apr 30, 2026

Upgrade prompt with migration guide: v3.0.0-beta.8 -> v3.0.0-beta.9
# Migration Guide: `v3.0.0-beta.8` -> `v3.0.0-beta.9`

This guide covers the user-facing changes introduced after commit `e2a8423` (`v3.0.0-beta.8`) up to `v3.0.0-beta.9`.

The two biggest changes are:

1. Animated GPU transitions for positions and visual attributes.
2. Replacement of the old imperative selection API with config-driven highlighting, outlining, and focusing.

## What Changed

### New features

- Animated transitions now apply to:
  - `setPointPositions()`
  - `setPointColors()`
  - `setPointSizes()`
  - `setLinkColors()`
  - `setLinkWidths()`
- New transition config:
  - `transitionDuration`
  - `transitionEasing`
  - `onTransitionStart`
  - `onTransition`
  - `onTransitionEnd`
- `enableSimulation` can now be toggled at runtime with `setConfig()` and `setConfigPartial()`.
- New config-driven interaction state:
  - `highlightedPointIndices`
  - `outlinedPointIndices`
  - `outlinedPointRingColor`
  - `highlightedLinkIndices`
  - `focusedLinkIndex`
  - `focusedLinkWidthIncrease`
- New graph query helpers:
  - `findPointsInRect(rect)`
  - `findPointsInPolygon(path)`
  - `getNeighboringPointIndices(pointIndices)`
  - `getConnectedLinkIndices(pointIndices)`
  - `getConnectedPointIndices(linkIndices)`

### Important behavior changes

- `transitionDuration` now defaults to `800`, so updates that used to snap in `beta.8` will animate in `beta.9`.
- When a position transition starts while the simulation is running, the simulation auto-pauses and stays paused after the transition ends. Call `graph.unpause()` to resume it.
- `fitView()` and `fitViewByPointIndices()` use the target positions during an active position transition, not the interpolated positions currently on screen.
- `start()` and `unpause()` interrupt an active position transition and continue from the current mid-transition positions.
- Repeated `start(alpha)` calls now reheat the simulation instead of behaving like a full restart cycle.

## Breaking Changes

### 1. Selection API was removed

These methods are no longer available:

- `selectPointByIndex()`
- `selectPointsByIndices()`
- `selectPointsInRect()`
- `selectPointsInPolygon()`
- `unselectPoints()`
- `getSelectedIndices()`

Use config-driven state instead.

Before:

```ts
graph.selectPointsByIndices([1, 2, 3])
graph.unselectPoints()
```

After:

```ts
graph.setConfigPartial({
  highlightedPointIndices: [1, 2, 3],
  highlightedLinkIndices: graph.getConnectedLinkIndices([1, 2, 3]),
})

graph.setConfigPartial({
  highlightedPointIndices: undefined,
  highlightedLinkIndices: undefined,
})
```

### 2. Point query methods were renamed

Before:

```ts
const points = graph.getPointsInRect([[10, 10], [200, 200]])
const polygonPoints = graph.getPointsInPolygon(path)
const adjacent = graph.getAdjacentIndices(42)
```

After:

```ts
await graph.ready

const points = graph.findPointsInRect([[10, 10], [200, 200]])
const polygonPoints = graph.findPointsInPolygon(path)
const neighbors = graph.getNeighboringPointIndices(42)
```

Notes:

- `findPointsInRect()` and `findPointsInPolygon()` now return `number[]` instead of `Float32Array`.
- Both methods should be called after `await graph.ready`.
- `getNeighboringPointIndices()` accepts either `number` or `number[]` and returns a deduplicated `number[]`.

### 3. `onPointMouseOver` signature changed

Before:

```ts
onPointMouseOver: (index, position, event, isSelected) => {
  if (isSelected) {
    // ...
  }
}
```

After:

```ts
onPointMouseOver: (index, position, event, isHighlighted, isOutlined) => {
  if (isHighlighted) {
    // ...
  }
  if (isOutlined) {
    // ...
  }
}
```

`isSelected` no longer exists because the selection API was removed.

### 4. Position updates animate by default

If your `beta.8` code relied on immediate snapping after `setPointPositions(...); render()`, that behavior changed.

Before:

```ts
graph.setPointPositions(nextPositions)
graph.render()
```

After, if you want the old snap behavior:

```ts
const graph = new Graph(container, {
  transitionDuration: 0,
})
```

If you want to keep transitions but handle the new auto-pause behavior explicitly:

```ts
graph.setPointPositions(nextPositions)
graph.render()

// In beta.9 the simulation stays paused after a position transition.
graph.unpause()
```

### 5. Config arrays are now stored by reference

`setConfig()` and `setConfigPartial()` no longer shallow-clone user-provided arrays such as `highlightedPointIndices`.

If you mutate an array after passing it into config, the graph may observe those changes.

Prefer this:

```ts
const highlighted = selectedIds.slice()
graph.setConfigPartial({ highlightedPointIndices: highlighted })
```

Avoid mutating the same array instance later:

```ts
const highlighted = [1, 2, 3]
graph.setConfigPartial({ highlightedPointIndices: highlighted })

highlighted.push(4) // avoid
```

## Migration Recipes

### Replace rectangular or polygon selection

Before:

```ts
graph.selectPointsInRect([[20, 20], [300, 200]])
graph.selectPointsInPolygon(path)
```

After:

```ts
await graph.ready

const pointIds = graph.findPointsInRect([[20, 20], [300, 200]])
const linkIds = graph.getConnectedLinkIndices(pointIds)

graph.setConfigPartial({
  highlightedPointIndices: pointIds,
  highlightedLinkIndices: linkIds,
})
```

Polygon version:

```ts
await graph.ready

const pointIds = graph.findPointsInPolygon(path)

graph.setConfigPartial({
  highlightedPointIndices: pointIds,
})
```

### Replace single-point selection plus neighbors

Before:

```ts
graph.selectPointByIndex(index, true)
```

After:

```ts
const pointIds = [index, ...graph.getNeighboringPointIndices(index)]
const uniquePointIds = Array.from(new Set(pointIds))

graph.setConfigPartial({
  highlightedPointIndices: uniquePointIds,
  highlightedLinkIndices: graph.getConnectedLinkIndices(uniquePointIds),
  focusedPointIndex: index,
})
```

### Add transitions intentionally

```ts
import { Graph, TransitionEasing } from '@cosmos.gl/graph'

const graph = new Graph(container, {
  transitionDuration: 1000,
  transitionEasing: TransitionEasing.CubicInOut,
  onTransitionStart: () => console.log('transition started'),
  onTransition: progress => console.log('progress', progress),
  onTransitionEnd: interrupted => console.log('ended', { interrupted }),
})

graph.setPointPositions(nextPositions)
graph.render()
```

### Handle `fitView()` during a position transition

Before, with snap updates, `fitView()` used the immediately-updated positions:

```ts
graph.setPointPositions(nextPositions)
graph.render()
graph.fitView()
```

In `beta.9`, if transitions are enabled, `fitView()` targets the final layout, not the in-between animated frame. If you need the old snap-then-fit behavior, disable the transition for that update:

```ts
graph.setConfigPartial({ transitionDuration: 0 })
graph.setPointPositions(nextPositions)
graph.render()
graph.fitView()
graph.setConfigPartial({ transitionDuration: 800 })
```

### Handle `start()` / `unpause()` during transitions

If your app resumes the simulation immediately after setting positions, be explicit about the new behavior:

```ts
graph.setPointPositions(nextPositions)
graph.render()

// Either wait for the transition to finish, then resume:
graph.unpause()
```

Or, if you want to cancel the active position transition and continue simulating from the current state:

```ts
graph.setPointPositions(nextPositions)
graph.render()
graph.start(1)
```

### Toggle simulation at runtime

```ts
graph.setConfigPartial({ enableSimulation: false })
graph.setConfigPartial({ enableSimulation: true })
```

Notes:

- `false -> true` recreates simulation resources.
- `true -> false` destroys simulation-only resources.
- Enabling simulation during an active transition interrupts that transition.

## Recommended Upgrade Checklist

1. Replace all removed selection methods with `setConfigPartial()` plus `highlightedPointIndices` / `highlightedLinkIndices`.
2. Rename `getPointsInRect()` to `findPointsInRect()` and `getPointsInPolygon()` to `findPointsInPolygon()`.
3. Rename `getAdjacentIndices()` to `getNeighboringPointIndices()`.
4. Update `onPointMouseOver` callbacks to use `(isHighlighted, isOutlined)` instead of `isSelected`.
5. Decide whether you want the new animated default or old snap behavior, and set `transitionDuration` explicitly.
6. If you pass arrays into config, stop mutating those arrays after the call.

## Summary

`v3.0.0-beta.9` introduces a much more declarative interaction model and adds first-class animated transitions. The main migration work is replacing the old selection methods and deciding whether your app should keep the old snap behavior by setting `transitionDuration: 0`.

@Stukova Stukova changed the title Add GPU transitions WIP Add GPU transitions May 13, 2026
Signed-off-by: Stukova Olya <stukova.o@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
migration-notes.md (1)

206-207: 💤 Low value

Consider using American English "afterward" instead of "afterwards".

The static analysis tool flagged this as a locale preference. "Afterward" is standard in American English documentation.

📝 Suggested fix
-**Auto-pause.** When a position transition runs while the simulation is on, the simulation auto-pauses for the transition and **stays paused afterwards**. Call `graph.unpause()` to resume forces. Set `transitionDuration: 0` to keep the v2 snap-and-keep-running behavior.
+**Auto-pause.** When a position transition runs while the simulation is on, the simulation auto-pauses for the transition and **stays paused afterward**. Call `graph.unpause()` to resume forces. Set `transitionDuration: 0` to keep the v2 snap-and-keep-running behavior.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@migration-notes.md` around lines 206 - 207, Update the phrasing in
migration-notes.md to use American English by replacing "afterwards" with
"afterward" in the Auto-pause note; keep the rest of the sentence and retain
references to graph.unpause() and transitionDuration: 0 so the instruction still
reads: call graph.unpause() to resume forces and set transitionDuration: 0 to
preserve v2 snap-and-keep-running behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/stories/2`. configuration.mdx:
- Line 110: The example for onTransitionEnd is misleading about
enableSimulation; update the sentence so it only cites enabling simulation
mid-flight (not toggling either direction) as an interruption example — e.g.,
"enabling enableSimulation mid-flight" — and keep transitionDuration being set
to 0 as a separate interruption example; modify the text around onTransitionEnd
to explicitly reference enableSimulation and transitionDuration to avoid
implying turning simulation off will interrupt transitions.

---

Nitpick comments:
In `@migration-notes.md`:
- Around line 206-207: Update the phrasing in migration-notes.md to use American
English by replacing "afterwards" with "afterward" in the Auto-pause note; keep
the rest of the sentence and retain references to graph.unpause() and
transitionDuration: 0 so the instruction still reads: call graph.unpause() to
resume forces and set transitionDuration: 0 to preserve v2 snap-and-keep-running
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2bc1f3d6-3016-4518-af11-c47dfca1c521

📥 Commits

Reviewing files that changed from the base of the PR and between fed8dcd and bdac751.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • .agents/skills/history/SKILL.md
  • .claude/skills/history/SKILL.md
  • history/2026/2026-04-22-gpu-transitions.md
  • migration-notes.md
  • package.json
  • src/index.ts
  • src/modules/Lines/index.ts
  • src/modules/Points/index.ts
  • src/modules/Shared/buffer.ts
  • src/modules/Transition/index.ts
  • src/stories/2. configuration.mdx
  • src/stories/3. api-reference.mdx
✅ Files skipped from review due to trivial changes (3)
  • package.json
  • .claude/skills/history/SKILL.md
  • history/2026/2026-04-22-gpu-transitions.md

| onSimulationUnpause | Called when simulation unpauses |
| onTransitionStart | Called when an animated transition starts (see [Transitions](#transitions)) |
| onTransition | Called on every transition frame with the eased `progress` value in the `[0, 1]` range: `(progress: number) => void` |
| onTransitionEnd | Called when a transition ends. `interrupted` is `true` when the transition was replaced or ended early (e.g. by setting `transitionDuration` to `0` mid-flight, or toggling `enableSimulation` mid-flight): `(interrupted: boolean) => void` |
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify the enableSimulation interruption example.

Line 110 currently reads as if either toggle direction interrupts transitions, but Line 149 says turning simulation off does not interrupt. Narrow the example to enabling simulation mid-flight.

📝 Suggested wording
-| onTransitionEnd | Called when a transition ends. `interrupted` is `true` when the transition was replaced or ended early (e.g. by setting `transitionDuration` to `0` mid-flight, or toggling `enableSimulation` mid-flight): `(interrupted: boolean) => void` |
+| onTransitionEnd | Called when a transition ends. `interrupted` is `true` when the transition was replaced or ended early (e.g. by setting `transitionDuration` to `0` mid-flight, or turning `enableSimulation` on mid-flight): `(interrupted: boolean) => void` |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| onTransitionEnd | Called when a transition ends. `interrupted` is `true` when the transition was replaced or ended early (e.g. by setting `transitionDuration` to `0` mid-flight, or toggling `enableSimulation` mid-flight): `(interrupted: boolean) => void` |
| onTransitionEnd | Called when a transition ends. `interrupted` is `true` when the transition was replaced or ended early (e.g. by setting `transitionDuration` to `0` mid-flight, or turning `enableSimulation` on mid-flight): `(interrupted: boolean) => void` |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stories/2`. configuration.mdx at line 110, The example for
onTransitionEnd is misleading about enableSimulation; update the sentence so it
only cites enabling simulation mid-flight (not toggling either direction) as an
interruption example — e.g., "enabling enableSimulation mid-flight" — and keep
transitionDuration being set to 0 as a separate interruption example; modify the
text around onTransitionEnd to explicitly reference enableSimulation and
transitionDuration to avoid implying turning simulation off will interrupt
transitions.

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.

1 participant