Skip to content

Conversation

reddygtvs
Copy link
Contributor

@reddygtvs reddygtvs commented Aug 26, 2025

Description

Added snap/alignment guidelines for frame positioning. Shows red lines when frames align to edges or centers, with 12px snap threshold. Ctrl/Cmd disables snapping.

Related Issues

related to #2702

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Release
  • Refactor
  • Other (please describe):

Testing

Screenshots (if applicable)

Screen.Recording.-.Aug.26.2025-VEED.mp4

Additional Notes

Open to making any changes related to styling (or otherwise). It was not defined in the feature request, so I ended up going with the standardised red/thin line/12 px snap offset based off what Figma/Adobe are doing for their own implementation.


Important

Adds snapping and alignment guidelines for frames with visual feedback and configurable settings in the canvas editor.

  • Behavior:
    • Adds snapping/alignment guidelines for frames in top-bar.tsx.
    • Red lines indicate alignment to edges or centers with a 12px snap threshold.
    • Snapping can be disabled with Ctrl/Cmd keys.
  • Components:
    • Introduces SnapGuidelines component in snap-guidelines.tsx for rendering snap lines.
    • Integrates SnapManager in engine.ts for managing snapping logic and configuration.
  • Configuration:
    • SnapManager handles snap configuration, including enabling/disabling and threshold settings.
    • Snap lines are shown/hidden based on alignment calculations.

This description was created by Ellipsis for f8b3eb6. You can customize this summary. It will automatically update as commits are pushed.

Summary by CodeRabbit

  • New Features
    • Snap-assisted dragging for frames to align with edges and centers of other elements.
    • Visual snap guidelines appear while dragging and auto-hide when finished.
    • Guidelines adapt to canvas zoom and pan for accurate placement.
    • Hold Ctrl/Cmd to temporarily disable snapping for free movement.
    • Snapped positions are preserved when released.

Copy link

vercel bot commented Aug 26, 2025

@reddygtvs is attempting to deploy a commit to the Onlook Team on Vercel.

A member of the Team first needs to authorize it.

Copy link

coderabbitai bot commented Aug 26, 2025

Walkthrough

Introduces a snapping system for frame dragging: adds SnapManager with config and active guideline lines, integrates it into EditorEngine, applies snap-assisted movement in the top-bar drag handler, renders snap guidelines via a new overlay component, and ensures guidelines hide during drag end and engine/UI clears.

Changes

Cohort / File(s) Summary
Drag snapping integration
apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar.tsx
Changes drag logic to allow snap-assisted position updates. Computes snap target via editorEngine.snap.calculateSnapTarget(...), applies snapped position, and shows/hides snap lines during drag; hides on drag end.
Overlay guidelines UI
apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/snap-guidelines.tsx, apps/web/client/src/app/project/[id]/_components/canvas/overlay/index.tsx
Adds SnapGuidelines observer component to render active snap lines scaled/transformed with canvas; imported and rendered by Overlay unconditionally.
Editor engine wiring
apps/web/client/src/components/store/editor/engine.ts
Adds snap: SnapManager to public API and hides snap lines in clear() and clearUI().
Snapping core logic
apps/web/client/src/components/store/editor/snap/index.ts
Introduces SnapManager with config, active lines state, target calculation across frames (edges/centers), candidate selection, and show/hide utilities.
Snap types
apps/web/client/src/components/store/editor/snap/types.ts
Adds public types: SnapBounds, SnapTarget, SnapLine, SnapFrame, SnapConfig, and SnapLineType enum.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant TopBar as TopBar Drag Handler
  participant Engine as EditorEngine
  participant Snap as SnapManager
  participant Storage as Frames Store
  participant Overlay as SnapGuidelines

  U->>TopBar: drag move (dx, dy, modifiers)
  TopBar->>Snap: calculateSnapTarget(frameId, position, dimension)
  alt snapping enabled and threshold matched and no Ctrl/Meta
    Snap-->>TopBar: SnapTarget(position', snapLines)
    TopBar->>Snap: showSnapLines(snapLines)
    TopBar->>Storage: updateAndSaveToStorage(frameId, { position: position' })
  else no snap
    Snap-->>TopBar: null
    TopBar->>Snap: hideSnapLines()
    TopBar->>Storage: updateAndSaveToStorage(frameId, { position })
  end
  note over Overlay,Snap: Overlay observes Snap.activeSnapLines and renders guidelines

  U-->>TopBar: drag end
  TopBar->>Snap: hideSnapLines()
Loading
sequenceDiagram
  participant App as App
  participant Engine as EditorEngine
  participant Snap as SnapManager
  participant Overlay as SnapGuidelines

  App->>Engine: clear()/clearUI()
  Engine->>Snap: hideSnapLines()
  Overlay-->>Overlay: activeSnapLines becomes empty → render null
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I hop between the frames with glee,
Red lines shimmer, guiding me—
Snap! goes left, then center true,
A gentle nudge, the canvas knew.
With whiskered math and tidy signs,
I map my world on glowing lines.
Thump-thump—aligned! ✅

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

return null;
}

snapCandidates.sort((a, b) => a.distance - b.distance);
Copy link
Contributor

Choose a reason for hiding this comment

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

The calculateSnapTarget function currently aggregates all candidate snaps but then returns only the best candidate overall. For improved UX, consider calculating and applying independent snapping adjustments for the horizontal and vertical axes (e.g. picking separate candidates for each axis) so that snapping can occur simultaneously in both dimensions.

Copy link

@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 (13)
apps/web/client/src/app/project/[id]/_components/canvas/overlay/index.tsx (1)

77-77: Consider z-order explicitly for guides (optional).

Guidelines render after OverlayButtons and may visually stack above them. With pointer-events: none this won’t break interaction, but if you want buttons to visually dominate, add a lower z-index to the guides container or higher z-index to buttons.

Example:

-            <SnapGuidelines />
+            <SnapGuidelines className="z-10" />

(Then accept an optional className prop in SnapGuidelines.)

apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar.tsx (1)

44-60: Snapping logic and Ctrl/Cmd override are implemented correctly; one UX caveat.

Behavior aligns with the PR description. However, actual snap sensitivity should feel constant in screen pixels regardless of zoom. That is handled inside SnapManager; ensure it uses a scale-adjusted threshold (see my comment in SnapManager).

If needed, you can also give users visual feedback that snapping is disabled while Ctrl/Cmd is held (e.g., temporarily dim guides or show a small “snap off” badge).

apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/snap-guidelines.tsx (2)

6-10: Avoid duplicating top bar metrics; move to a shared constant.

TOP_BAR_HEIGHT and TOP_BAR_MARGIN are also encoded in TopBar. Drift will misplace lines if either changes. Centralize these values (e.g., a shared visual-config) and import them here and in TopBar.

Add a small shared module (example):

// apps/web/client/src/components/store/editor/snap/visual-config.ts
export const SNAP_VISUAL_CONFIG = {
  TOP_BAR_HEIGHT: 28,
  TOP_BAR_MARGIN: 10,
} as const;

Then import it here and use it in TopBar.


22-67: Recheck the horizontal “visualOffset” logic across scales and alignment types.

Adding (TOP_BAR_HEIGHT + TOP_BAR_MARGIN) / scale to all horizontal guides assumes every horizontal line should be pushed down by the top-bar stack. If the line’s position already reflects frame bounds (content), this may double-offset; if it reflects the frame’s outer box, it may be correct. Please verify at scales 0.5x, 1x, 2x for:

  • top-edge alignment,
  • bottom-edge alignment,
  • center-horizontal alignment.

If misaligned, consider deriving an explicit “frameOuterToContentYOffset” from the same place that positions TopBar and applying it only when the snapped edge is the top edge. Alternatively, render guidelines relative to the exact rects used to compute snapping to avoid ad-hoc offsets.

apps/web/client/src/components/store/editor/snap/index.ts (3)

75-88: Avoid sorting all candidates on every mousemove; track the best in linear time.

This reduces allocations and CPU when many frames exist.

Example change (illustrative, same behavior):

-        const snapCandidates: Array<{ position: RectPosition; lines: SnapLine[]; distance: number }> = [];
-
-        for (const otherFrame of otherFrames) {
-            const candidates = this.calculateSnapCandidates(dragBounds, otherFrame);
-            snapCandidates.push(...candidates);
-        }
-
-        if (snapCandidates.length === 0) {
-            return null;
-        }
-
-        snapCandidates.sort((a, b) => a.distance - b.distance);
-        const bestCandidate = snapCandidates[0];
+        let bestCandidate: { position: RectPosition; lines: SnapLine[]; distance: number } | null = null;
+        for (const otherFrame of otherFrames) {
+            const candidates = this.calculateSnapCandidates(dragBounds, otherFrame);
+            for (const c of candidates) {
+                if (!bestCandidate || c.distance < bestCandidate.distance) {
+                    bestCandidate = c;
+                    if (bestCandidate.distance === 0) break; // early exit exact match
+                }
+            }
+        }
+        if (!bestCandidate) {
+            return null;
+        }

98-103: Support showing both axes when both snap within threshold (optional).

Tools typically snap independently on X and Y; if both axes qualify, render both guidelines. Current code returns only lines[0].

One approach:

  • Track best horizontal and best vertical candidates separately.
  • Combine them if both within threshold before returning.

If you prefer minimal changes, keep the current single-line behavior for now.

Also applies to: 226-231


215-223: Stabilize guideline keys to reduce DOM churn (optional).

Using Date.now() changes keys every frame; React will recreate nodes unnecessarily. Encode identity from deterministic parts (type, target frame, rounded position).

Apply this diff:

-        return {
-            id: `${type}-${otherFrame.id}-${Date.now()}`,
+        return {
+            id: `${type}-${otherFrame.id}-${Math.round(position)}`,
             type,
             orientation,
             position,
             start,
             end,
             frameIds: [otherFrame.id],
         };
apps/web/client/src/components/store/editor/snap/types.ts (6)

3-12: SnapBounds duplicates derivable geometry; consider immutability and documenting invariants

left/top/right/bottom and centerX/centerY/width/height can drift if any are computed separately. Either document invariants (single source of truth) or expose only one representation and derive the rest at use sites. Also, marking this shape as immutable helps avoid accidental mutation during drag.

Apply immutability via readonly:

-export interface SnapBounds {
-    left: number;
-    top: number;
-    right: number;
-    bottom: number;
-    centerX: number;
-    centerY: number;
-    width: number;
-    height: number;
-}
+export interface SnapBounds {
+    readonly left: number;
+    readonly top: number;
+    readonly right: number;
+    readonly bottom: number;
+    readonly centerX: number;
+    readonly centerY: number;
+    readonly width: number;
+    readonly height: number;
+}

If you already compute bounds from RectPosition/RectDimension in SnapManager, consider removing either width/height or centerX/centerY here to enforce one source of truth.


14-18: “distance” is ambiguous; capture snap delta per axis and make fields readonly

A scalar distance doesn’t convey whether we snapped on X, Y, or both. Explicit dx/dy improves intent and avoids sign ambiguity. Also, make this immutable and the lines array readonly.

-export interface SnapTarget {
-    position: RectPosition;
-    snapLines: SnapLine[];
-    distance: number;
-}
+export interface SnapTarget {
+    readonly position: RectPosition;
+    readonly snapLines: readonly SnapLine[];
+    /** Total scalar distance used to rank candidates (retain for compatibility). */
+    readonly distance: number;
+    /** Pixel delta to apply to reach the snapped position. */
+    readonly offset: { dx: number; dy: number };
+}

Confirm the drag code in top-bar uses both axes; if it only snaps along one axis per interaction, we can make offset one-dimensional conditionally.


20-28: Avoid type/orientation inconsistencies; add immutability and optional spacing value

orientation is derivable from type (e.g., EDGE_TOP implies horizontal). Keeping both risks mismatch. If you keep orientation, add a runtime assert or derive it in a helper. Also, make fields readonly. For spacing guidelines, consider carrying the spacing amount.

-export interface SnapLine {
-    id: string;
-    type: SnapLineType;
-    orientation: 'horizontal' | 'vertical';
-    position: number;
-    start: number;
-    end: number;
-    frameIds: string[];
-}
+export interface SnapLine {
+    readonly id: string;
+    readonly type: SnapLineType;
+    readonly orientation: 'horizontal' | 'vertical';
+    /** Primary axis coordinate of the guide line. */
+    readonly position: number;
+    /** Extents along the secondary axis. */
+    readonly start: number;
+    readonly end: number;
+    /** For SPACING lines, the spacing amount in pixels. */
+    readonly spacing?: number;
+    readonly frameIds: readonly string[];
+}

If you prefer to eliminate the mismatch entirely, consider a discriminated union keyed by orientation:

type Orientation = 'horizontal' | 'vertical';

type HorizontalSnapLine = Readonly<{
  id: string;
  type: Extract<SnapLineType, 'edge-top' | 'edge-bottom' | 'center-horizontal' | 'spacing'>;
  orientation: 'horizontal';
  position: number; // y
  start: number;    // x1
  end: number;      // x2
  spacing?: number;
  frameIds: readonly string[];
}>;

type VerticalSnapLine = Readonly<{
  id: string;
  type: Extract<SnapLineType, 'edge-left' | 'edge-right' | 'center-vertical' | 'spacing'>;
  orientation: 'vertical';
  position: number; // x
  start: number;    // y1
  end: number;      // y2
  spacing?: number;
  frameIds: readonly string[];
}>;

export type SnapLine = HorizontalSnapLine | VerticalSnapLine;

30-38: Prefer string-literal union over runtime enum for shared libs

Using a TypeScript enum emits runtime code; a union keeps this file type-only and tree-shakeable.

-export enum SnapLineType {
-    EDGE_LEFT = 'edge-left',
-    EDGE_RIGHT = 'edge-right',
-    EDGE_TOP = 'edge-top',
-    EDGE_BOTTOM = 'edge-bottom',
-    CENTER_HORIZONTAL = 'center-horizontal',
-    CENTER_VERTICAL = 'center-vertical',
-    SPACING = 'spacing',
-}
+export type SnapLineType =
+    | 'edge-left'
+    | 'edge-right'
+    | 'edge-top'
+    | 'edge-bottom'
+    | 'center-horizontal'
+    | 'center-vertical'
+    | 'spacing';

Optional helper (outside this file) to derive orientation from type:

export const orientationFromType = (t: SnapLineType): 'horizontal' | 'vertical' =>
  t === 'edge-top' || t === 'edge-bottom' || t === 'center-horizontal' ? 'horizontal' : 'vertical';

40-45: SnapFrame doubles state with bounds + position/dimension; make immutable or document derivation

Carrying both position/dimension and bounds introduces drift risk on updates. If bounds are cached for perf, document that they’re derived and refresh together; otherwise consider deriving on read. Also, mark fields readonly.

-export interface SnapFrame {
-    id: string;
-    position: RectPosition;
-    dimension: RectDimension;
-    bounds: SnapBounds;
-}
+export interface SnapFrame {
+    readonly id: string;
+    readonly position: RectPosition;
+    readonly dimension: RectDimension;
+    readonly bounds: SnapBounds;
+}

47-51: Config is missing the “Ctrl/Cmd disables snapping” behavior; add typed modifiers and defaults

PR summary states Ctrl/Cmd disables snapping. Encode that here to avoid scattering UI-specific checks. Also, readonly fields help treat config as value objects.

-export interface SnapConfig {
-    threshold: number;
-    enabled: boolean;
-    showGuidelines: boolean;
-}
+export interface SnapConfig {
+    /** Snap activation threshold in pixels. Default: 12. */
+    readonly threshold: number;
+    readonly enabled: boolean;
+    readonly showGuidelines: boolean;
+    /** Modifier keys that temporarily disable snapping (KeyboardEvent.key values). */
+    readonly disableWithModifiers?: ReadonlyArray<'Meta' | 'Control'>;
+}

Optionally centralize defaults in a constants module:

// apps/web/client/src/components/store/editor/snap/constants.ts
export const DEFAULT_SNAP_THRESHOLD = 12;
export const DEFAULT_DISABLE_WITH_MODIFIERS: ReadonlyArray<'Meta' | 'Control'> = ['Meta', 'Control'];

I can follow through by updating SnapManager initialization to use these defaults and wire the modifier check to this config if you want.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6e072e5 and f8b3eb6.

📒 Files selected for processing (6)
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/snap-guidelines.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/canvas/overlay/index.tsx (2 hunks)
  • apps/web/client/src/components/store/editor/engine.ts (3 hunks)
  • apps/web/client/src/components/store/editor/snap/index.ts (1 hunks)
  • apps/web/client/src/components/store/editor/snap/types.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
apps/web/client/src/app/project/[id]/_components/canvas/overlay/index.tsx (1)
apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/snap-guidelines.tsx (1)
  • SnapGuidelines (11-67)
apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/snap-guidelines.tsx (1)
apps/web/client/src/components/store/editor/index.tsx (1)
  • useEditorEngine (9-13)
apps/web/client/src/components/store/editor/snap/index.ts (2)
apps/web/client/src/components/store/editor/snap/types.ts (5)
  • SnapConfig (47-51)
  • SnapLine (20-28)
  • SnapBounds (3-12)
  • SnapFrame (40-45)
  • SnapTarget (14-18)
apps/web/client/src/components/store/editor/engine.ts (1)
  • EditorEngine (32-115)
apps/web/client/src/components/store/editor/engine.ts (1)
apps/web/client/src/components/store/editor/snap/index.ts (1)
  • SnapManager (12-240)
🔇 Additional comments (7)
apps/web/client/src/components/store/editor/engine.ts (3)

25-25: Good integration: SnapManager is part of the EditorEngine public surface.

Import and initialization pattern are consistent with other managers. Positioning snap after frames ensures dependencies are available during construction.

Also applies to: 63-63


96-97: Hide guidelines on full clear is correct.

Clearing snap lines during clear() prevents stale guides when projects switch or engine resets.


103-104: UI clear also hides guidelines — consistent with UX expectations.

This avoids leftover guides after selection/UI resets.

apps/web/client/src/app/project/[id]/_components/canvas/overlay/index.tsx (1)

13-13: Overlay now renders SnapGuidelines — import looks correct.

No side effects on existing overlay logic.

apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar.tsx (2)

39-43: Mutable newPosition to accommodate snapping is appropriate.

This sets up the subsequent snap adjustment correctly.


67-67: Hiding guides on drag end is correct.

Prevents ghost lines after interaction.

apps/web/client/src/components/store/editor/snap/types.ts (1)

1-1: Good use of import type

Using import type avoids bundling runtime code and keeps this module purely declarative.

Comment on lines +7 to +10
const SNAP_CONFIG = {
DEFAULT_THRESHOLD: 12,
LINE_EXTENSION: 160,
} as const;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make snap threshold screen-space aware (essential for consistent UX).

The PR promises a 12px threshold akin to Figma/Adobe. Current code compares distances in canvas coordinates to a fixed 12. This makes snapping too “sticky” when zoomed out and too “loose” when zoomed in. Use an effective threshold = configuredPx / canvas.scale.

Apply this diff:

@@
 const SNAP_CONFIG = {
-    DEFAULT_THRESHOLD: 12,
+    DEFAULT_THRESHOLD: 12, // in screen pixels
     LINE_EXTENSION: 160,
 } as const;
@@
 export class SnapManager {
@@
     calculateSnapTarget(
         dragFrameId: string,
         currentPosition: RectPosition,
         dimension: RectDimension,
     ): SnapTarget | null {
-        if (!this.config.enabled) {
+        if (!this.config.enabled) {
             return null;
         }
 
-        const dragBounds = this.createSnapBounds(currentPosition, dimension);
+        const dragBounds = this.createSnapBounds(currentPosition, dimension);
+        // Convert 12px screen threshold to canvas-space
+        const effectiveThreshold = this.config.threshold / Math.max(1e-6, this.editorEngine.canvas.scale);
@@
-        snapCandidates.sort((a, b) => a.distance - b.distance);
-        const bestCandidate = snapCandidates[0];
+        snapCandidates.sort((a, b) => a.distance - b.distance);
+        const bestCandidate = snapCandidates[0];
 
-        if (!bestCandidate || bestCandidate.distance > this.config.threshold) {
+        if (!bestCandidate || bestCandidate.distance > effectiveThreshold) {
             return null;
         }
@@
-            if (distance <= this.config.threshold) {
+            if (distance <= effectiveThreshold) {
                 const offset = alignment.targetValue - alignment.dragOffset;
                 const newPosition = alignment.orientation === 'horizontal' 
                     ? { x: dragBounds.left, y: dragBounds.top + offset }
                     : { x: dragBounds.left + offset, y: dragBounds.top };
 
                 const snapLine = this.createSnapLine(alignment.type, alignment.orientation, alignment.targetValue, otherFrame, dragBounds);

Also applies to: 59-66, 86-92, 174-192

🤖 Prompt for AI Agents
In apps/web/client/src/components/store/editor/snap/index.ts around lines 7-10
(and also update occurrences at 59-66, 86-92, 174-192), the snap threshold is a
fixed canvas-space value which makes snapping inconsistent across zoom levels;
compute an effective threshold in canvas units by dividing the configured
screen-space pixels by the current canvas.scale (effectiveThreshold =
CONFIGURED_PX / canvas.scale) and use that effectiveThreshold in all distance
comparisons and range checks (including any LINE_EXTENSION-related comparisons
where thresholds are applied) so snap behavior remains consistent regardless of
zoom.

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