Skip to content

feat(camera): add committed $camera signal and Layer.onCameraChange hook#303

Merged
draedful merged 2 commits into
mainfrom
feat/camera-committed-signal
Jun 29, 2026
Merged

feat(camera): add committed $camera signal and Layer.onCameraChange hook#303
draedful merged 2 commits into
mainfrom
feat/camera-committed-signal

Conversation

@draedful

@draedful draedful commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Introduce graph.$camera signal as the source of committed camera state, updated only after a non-prevented camera-change event
  • Add Layer.onCameraChange() hook — base Layer subscribes to $camera and applies built-in transforms before calling the override
  • Add useSignalLayoutEffect React hook; migrate useSceneChange, internal layers, and camera auto-panning sync to committed state
  • Document the proposed vs committed model (camera-change event vs $camera / cameraService) and add e2e tests for the contract

Test plan

  • Unit tests: useSignal.test.ts (including useSignalLayoutEffect)
  • E2E: e2e/tests/camera/camera-change-signal.spec.ts
  • Manual: pan/zoom graph — layers (minimap, devtools ruler) and React overlays update correctly
  • Manual: preventDefault() on camera-change cancels commit and keeps $camera unchanged
  • Storybook: Playground toolbox scale indicator follows zoom via useSignal(graph.$camera)

Made with Cursor

Summary by Sourcery

Introduce a committed camera signal and hook layers and React utilities into it for camera-driven updates.

New Features:

  • Add graph.$camera signal to expose committed camera state after non-prevented camera-change events.
  • Add Layer.onCameraChange hook for reacting to committed camera updates after built-in transforms.
  • Introduce useSignalLayoutEffect React hook for layout-synchronous signal-driven side effects.

Enhancements:

  • Refactor layers, camera auto-panning, drag listener, minimap, devtools, and scene-change logic to subscribe to the committed camera signal instead of raw camera-change events.
  • Clarify React and system documentation to distinguish proposed camera-change events from committed $camera state and update usage examples accordingly.

Tests:

  • Extend unit tests to cover useSignalLayoutEffect behavior.
  • Add e2e helpers and a new camera-change vs $camera signal e2e spec and index it in the e2e README.

@draedful draedful requested a review from Antamansid as a code owner June 26, 2026 13:04
@sourcery-ai

sourcery-ai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements a committed camera state signal graph.$camera, updates CameraService and Graph to maintain it, switches Layer and multiple layers/utilities from camera-change event subscriptions to the committed signal via a new Layer.onCameraChange hook, adds a React useSignalLayoutEffect hook and migrates React integrations to use committed camera state, and documents/tests the proposed vs committed camera model.

File-Level Changes

Change Details Files
Introduce graph.$camera committed camera state and wire it into CameraService and Graph APIs.
  • Add graph.$camera signal initialized with getInitCameraState to Graph.
  • Update CameraService.set to compute nextState, execute the preventable camera-change default action, and on success update internal state, recompute relative coordinates, and sync graph.$camera.
  • Implement CameraService.syncCameraSignal to copy committed internal camera state into graph.$camera with a cloned viewportInsets object.
src/graph.ts
src/services/camera/CameraService.ts
Change Layer to subscribe to committed camera state via graph.$camera and expose a new onCameraChange hook after applying built-in transforms.
  • Replace Layer afterInit camera-change event subscription with a signal subscription using onSignal(graph.$camera, ...).
  • Introduce handleCommittedCameraChange to apply camera-based HTML/canvas transforms then delegate to overridable onCameraChange.
  • Refactor initial camera handling in afterInit to go through handleCommittedCameraChange and add documentation comments for the new onCameraChange hook.
src/services/Layer.ts
docs/rendering/layers.md
docs/system/events.md
docs/system/camera.md
.cursor/rules/layer-rules.mdc
Migrate internal layers, camera auto-panning, and utilities from camera-change events to committed camera signal or Layer.onCameraChange.
  • GraphLayer stops listening directly to camera-change and instead uses onCameraChange to perform mouse event emulation based on committed camera state.
  • GridLayer, MiniMapLayer, DevToolsLayer, PortConnectionLayer override onCameraChange to trigger re-rendering or invalidate internal structures.
  • Camera component auto-panning and dragListener auto-panning sync now subscribe to graph.$camera instead of camera-change events, with proper unsubscription.
src/components/canvas/layers/graphLayer/GraphLayer.ts
src/components/canvas/layers/portConnectionLayer/PortConnectionLayer.ts
src/plugins/minimap/layer.ts
src/plugins/devtools/DevToolsLayer.ts
src/services/camera/Camera.ts
src/utils/functions/dragListener.ts
.cursor/rules/event-model-rules.mdc
Add useSignalLayoutEffect React hook and migrate React-facing APIs and examples to use committed camera state instead of raw camera-change events.
  • Introduce useSignalLayoutEffect in useSignal.ts implemented via useLayoutEffect and effect(()=>handle()).
  • Add tests for useSignalLayoutEffect behavior to useSignal.test.ts.
  • Update useSceneChange to debounce on graph.$camera via a layout effect and hitTest update events, and simplify its lifecycle.
  • Refactor docs and examples to prefer graph.$camera and useSignal/useSignalLayoutEffect over camera-change debounced/throttled event handlers, including minimap overlay, CameraInfo, Toolbox scale indicator, and scene-change description.
src/react-components/hooks/useSignal.ts
src/react-components/hooks/useSignal.test.ts
src/react-components/hooks/useSceneChange.ts
docs/react/hooks.md
docs/react/usage.md
src/stories/Playground/Toolbox.tsx
Document the proposed vs committed camera model and extend e2e support around camera signal behavior.
  • Expand camera.md and events.md to describe the preventable camera-change event, the committed graph.$camera signal, and guidelines for choosing between event, signal, and cameraService.
  • Adjust Layer documentation examples to use onCameraChange and optional direct $camera subscriptions instead of camera-change listeners.
  • Update e2e README with new camera test files and add GraphPageObject helpers for reading and collecting graph.$camera snapshots.
  • Introduce a new e2e spec for camera-change vs $camera signal behavior (file stub added).
docs/system/camera.md
docs/system/events.md
docs/rendering/layers.md
docs/react/hooks.md
docs/react/usage.md
e2e/README.md
e2e/page-objects/GraphPageObject.ts
e2e/tests/camera/camera-change-signal.spec.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/components/canvas/layers/graphLayer/GraphLayer.ts" line_range="118-120" />
<code_context>
+         super.afterInit(); // subscribes to $camera and calls onCameraChange()
+       }
+
+       protected onCameraChange(camera: TCameraState): void {
+         this.handleCameraChange(camera);
        }
</code_context>
<issue_to_address>
**issue (bug_risk):** GraphLayer no longer triggers performRender on camera changes, which may drop visual updates.

Before, this layer subscribed to `camera-change` and called `this.performRender()` on each update. Now, via `$camera`, `Layer` routes camera changes to `handleCommittedCameraChange`, which only calls `applyCameraTransform` and `onCameraChange` and doesn’t trigger `performRender`. This override only forwards the camera to `handleCameraChange`, so camera changes might no longer cause the graph canvas to re-render. Please either invoke `this.performRender()` from `onCameraChange` here, or confirm that rendering is driven from another explicit camera-based mechanism.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/components/canvas/layers/graphLayer/GraphLayer.ts
@gravity-ui-bot

Copy link
Copy Markdown
Contributor

Preview is ready.

Separate proposed camera state (camera-change event) from committed state
(graph.$camera signal). Layers and React hooks react to applied changes;
camera-change remains for interception via preventDefault().

Co-authored-by: Cursor <cursoragent@cursor.com>
@draedful draedful force-pushed the feat/camera-committed-signal branch from e8757ef to e930a47 Compare June 28, 2026 23:11
Remove nonexistent graph.cameraState from camera docs and sync the overlay via useSceneChange instead of camera-change events.

Co-authored-by: Cursor <cursoragent@cursor.com>
@draedful draedful merged commit 12c24d3 into main Jun 29, 2026
8 checks passed
@draedful draedful deleted the feat/camera-committed-signal branch June 29, 2026 00:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants