Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8f247c7
Implement angle snapping for drag functionality
S-A-Adit Jan 10, 2026
5317789
Add SnapOverlay for drag snapping functionality
S-A-Adit Jan 10, 2026
5671359
Add unit tests for SnapOverlay functionality
S-A-Adit Jan 10, 2026
61d75f0
Change export from tools to snap-overlay
S-A-Adit Jan 10, 2026
c1e1ccd
Update SnapOverlay import path
S-A-Adit Jan 10, 2026
fcb311c
Update snap overlay tests to use initial distance
S-A-Adit Jan 10, 2026
8f8f4d6
Export tools from index.ts
S-A-Adit Jan 11, 2026
4463a6d
Re-add export from './tools' in index.ts
S-A-Adit Jan 11, 2026
1df6599
Refactor shift key check for snapping logic
S-A-Adit Jan 11, 2026
b447ac3
Merge branch 'toeverything:canary' into feature/shift-angle-snapping
S-A-Adit Jan 20, 2026
371cf14
Refactor drag position calculation with snapping
S-A-Adit Jan 22, 2026
085700c
Refactor drag move logic in manager.ts
S-A-Adit Jan 22, 2026
26b14e1
Snap drag angle during drag end context update
S-A-Adit Jan 22, 2026
8eaee45
Refactor snapOverlay initialization and handling
S-A-Adit Jan 23, 2026
2e6c415
Handle optional snapOverlay in drag end context
S-A-Adit Jan 23, 2026
cb90dcd
Fix snapDragAngle call and ensure finalPoint assignment
S-A-Adit Jan 24, 2026
b32c412
Refactor canvas event handler initialization
S-A-Adit Jan 24, 2026
ac68625
Use optional chaining for canvasEventHandler dispatch
S-A-Adit Jan 24, 2026
4e7b05a
Use optional chaining for snapDragAngle method
S-A-Adit Jan 24, 2026
e87cce2
Merge branch 'canary' into feature/shift-angle-snapping
S-A-Adit Jan 25, 2026
7b914ee
Merge branch 'canary' into feature/shift-angle-snapping
S-A-Adit Jan 25, 2026
6b0e89b
Merge branch 'canary' into feature/shift-angle-snapping
S-A-Adit Jan 26, 2026
a451f99
Merge branch 'canary' into feature/shift-angle-snapping
S-A-Adit Jan 27, 2026
daf3a71
Refactor handleElementRotate and handleElementResize methods
S-A-Adit Feb 2, 2026
471a0b6
Refactor scale calculations in manager.ts
S-A-Adit Feb 5, 2026
de6ed67
Update scale calculations to use snapped positions
S-A-Adit Feb 5, 2026
932a09f
Refactor rotation and resize event handlers
S-A-Adit Feb 5, 2026
12dbb0f
Improve snap resizing logic with anchor point
S-A-Adit Feb 5, 2026
990bd03
Refactor local space conversion for snapping logic
S-A-Adit Feb 5, 2026
d63daa3
Add null check for matrix in handle function
S-A-Adit Feb 9, 2026
ff6c7f3
Fix matrix inversion check before applying transform
S-A-Adit Feb 9, 2026
7c5cb46
Merge branch 'canary' into feature/shift-angle-snapping
S-A-Adit Feb 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Point } from '@blocksuite/global/gfx';
import { SnapOverlay } from '../snap-overlay';
import { GfxController } from '@blocksuite/std/gfx';

describe('SnapOverlay', () => {
let snapOverlay: SnapOverlay;
let mockGfxController: GfxController;

beforeEach(() => {
// Mock GfxController
mockGfxController = {
viewport: {
zoom: 1,
},
grid: {
search: () => new Set(),
},
} as GfxController;
snapOverlay = new SnapOverlay(mockGfxController);
});

describe('snapDragAngle', () => {
const startPoint = new Point(0, 0);

it('should not snap if shift is not pressed', () => {
const currentPoint = new Point(10, 5);
const snappedPoint = snapOverlay.snapDragAngle(startPoint, currentPoint, false);
expect(snappedPoint.x).toBeCloseTo(currentPoint.x);
expect(snappedPoint.y).toBeCloseTo(currentPoint.y);
});

it('should return currentPoint if start and current points are identical', () => {
const currentPoint = new Point(0, 0);
const snappedPoint = snapOverlay.snapDragAngle(startPoint, currentPoint, true);
expect(snappedPoint.x).toBeCloseTo(currentPoint.x);
expect(snappedPoint.y).toBeCloseTo(currentPoint.y);
});

it('should snap to 0 degrees (horizontal) when dragging right with shift', () => {
const currentPoint = new Point(10, 2);
const initialDistance = Math.hypot(currentPoint.x - startPoint.x, currentPoint.y - startPoint.y);
const snappedPoint = snapOverlay.snapDragAngle(startPoint, currentPoint, true);
expect(snappedPoint.x).toBeCloseTo(initialDistance);
expect(snappedPoint.y).toBeCloseTo(0);
});

it('should snap to 45 degrees when dragging with shift near 45', () => {
const currentPoint = new Point(10, 10 * Math.tan(Math.PI / 4 + Math.PI / 100));
const snappedPoint = snapOverlay.snapDragAngle(startPoint, currentPoint, true);
expect(snappedPoint.x).toBeCloseTo(10);
expect(snappedPoint.y).toBeCloseTo(10);
});

it('should snap to 90 degrees (vertical) when dragging down with shift', () => {
const currentPoint = new Point(2, 10);
const initialDistance = Math.hypot(currentPoint.x - startPoint.x, currentPoint.y - startPoint.y);
const snappedPoint = snapOverlay.snapDragAngle(startPoint, currentPoint, true);
expect(snappedPoint.x).toBeCloseTo(0);
expect(snappedPoint.y).toBeCloseTo(initialDistance);
});

it('should snap to 180 degrees (horizontal) when dragging left with shift', () => {
const currentPoint = new Point(-10, 2);
const initialDistance = Math.hypot(currentPoint.x - startPoint.x, currentPoint.y - startPoint.y);
const snappedPoint = snapOverlay.snapDragAngle(startPoint, currentPoint, true);
expect(snappedPoint.x).toBeCloseTo(-initialDistance);
expect(snappedPoint.y).toBeCloseTo(0);
});

it('should maintain distance after snapping', () => {
const currentPoint = new Point(10, 5);
const initialDistance = Math.sqrt(10 * 10 + 5 * 5);
const snappedPoint = snapOverlay.snapDragAngle(startPoint, currentPoint, true);
const snappedDistance = Math.sqrt(
(snappedPoint.x - startPoint.x) * (snappedPoint.x - startPoint.x) +
(snappedPoint.y - startPoint.y) * (snappedPoint.y - startPoint.y)
);
expect(snappedDistance).toBeCloseTo(initialDistance);
});

it('should snap to -15 degrees (345 degrees) when dragging near negative 15 with shift', () => {
const currentPoint = new Point(10 * Math.cos(-Math.PI / 12 - Math.PI / 100), 10 * Math.sin(-Math.PI / 12 - Math.PI / 100));
const snappedPoint = snapOverlay.snapDragAngle(startPoint, currentPoint, true);
expect(snappedPoint.x).toBeCloseTo(10 * Math.cos(-Math.PI / 12));
expect(snappedPoint.y).toBeCloseTo(10 * Math.sin(-Math.PI / 12));
});
});
});
1 change: 1 addition & 0 deletions blocksuite/affine/gfx/pointer/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './snap/snap-overlay';
export * from './tools';
32 changes: 32 additions & 0 deletions blocksuite/affine/gfx/pointer/src/snap/snap-overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,38 @@ export class SnapOverlay extends Overlay {
all: [],
};

/**
* Snaps a current point to an angle interval relative to a start point if the shift key is pressed.
*
* @param startPoint The starting point of the drag.
* @param currentPoint The current point of the drag.
* @param isShiftPressed True if the SHIFT key is currently pressed, false otherwise.
* @returns The snapped point if shift is pressed, otherwise the original currentPoint.
*/
snapDragAngle(startPoint: Point, currentPoint: Point, isShiftPressed: boolean): Point {
if (!isShiftPressed) {
return currentPoint;
}

const ANGLE_SNAP_INTERVAL_RAD = Math.PI / 12; // 15 degrees in radians

const dx = currentPoint.x - startPoint.x;
const dy = currentPoint.y - startPoint.y;

if (dx === 0 && dy === 0) {
return currentPoint; // Avoid division by zero when points are identical
}

const currentAngle = Math.atan2(dy, dx);
const snappedAngle = Math.round(currentAngle / ANGLE_SNAP_INTERVAL_RAD) * ANGLE_SNAP_INTERVAL_RAD;
const distance = Math.sqrt(dx * dx + dy * dy);

const snappedX = startPoint.x + distance * Math.cos(snappedAngle);
const snappedY = startPoint.y + distance * Math.sin(snappedAngle);

return new Point(snappedX, snappedY);
}

/**
* This variable contains reference lines that are
* generated by the 'Distribute Alignment' function. This alignment is achieved
Expand Down
Loading