From 6ea5a156b6b7f609354a4563cfb2d00ec7f41811 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 29 Apr 2026 18:23:08 +0200 Subject: [PATCH] refactor: split mask snap dispatch Made-with: Cursor --- apps/web/src/masks/__tests__/snap.test.ts | 12 +-- apps/web/src/masks/definitions/box-like.ts | 4 +- apps/web/src/masks/definitions/split.ts | 4 +- apps/web/src/masks/snap.ts | 110 ++++++++++++++------- 4 files changed, 85 insertions(+), 45 deletions(-) diff --git a/apps/web/src/masks/__tests__/snap.test.ts b/apps/web/src/masks/__tests__/snap.test.ts index 2f33615f..510c6a27 100644 --- a/apps/web/src/masks/__tests__/snap.test.ts +++ b/apps/web/src/masks/__tests__/snap.test.ts @@ -13,7 +13,7 @@ import { import { getSplitMaskStrokeSegment } from "@/masks/definitions/split"; import { textMaskDefinition } from "@/masks/definitions/text"; import { getMaskSnapGeometry } from "@/masks/geometry"; -import { snapMaskInteraction } from "@/masks/snap"; +import { snapBoxMaskInteraction, snapSplitMaskInteraction } from "@/masks/snap"; import type { ElementBounds } from "@/preview/element-bounds"; import type { CustomMaskParams, @@ -230,7 +230,7 @@ describe("mask geometry", () => { describe("mask snapping", () => { test("snaps split mask movement using the shared position pipeline", () => { - const result = snapMaskInteraction({ + const result = snapSplitMaskInteraction({ handleId: "position", startParams: buildSplitParams({ centerX: 0.03, @@ -254,7 +254,7 @@ describe("mask snapping", () => { }); test("snaps box mask movement against element center and edges", () => { - const result = snapMaskInteraction({ + const result = snapBoxMaskInteraction({ handleId: "position", startParams: buildRectangleParams(), proposedParams: buildRectangleParams({ @@ -275,7 +275,7 @@ describe("mask snapping", () => { }); test("snaps mask rotation through the shared rotation path", () => { - const result = snapMaskInteraction({ + const result = snapBoxMaskInteraction({ handleId: "rotation", startParams: buildRectangleParams(), proposedParams: buildRectangleParams({ @@ -291,7 +291,7 @@ describe("mask snapping", () => { }); test("snaps edge resize for box masks", () => { - const result = snapMaskInteraction({ + const result = snapBoxMaskInteraction({ handleId: "right", startParams: buildRectangleParams(), proposedParams: buildRectangleParams({ @@ -307,7 +307,7 @@ describe("mask snapping", () => { }); test("snaps corner resize for box masks", () => { - const result = snapMaskInteraction({ + const result = snapBoxMaskInteraction({ handleId: "bottom-right", startParams: buildRectangleParams(), proposedParams: buildRectangleParams({ diff --git a/apps/web/src/masks/definitions/box-like.ts b/apps/web/src/masks/definitions/box-like.ts index 7db51df7..5462c14f 100644 --- a/apps/web/src/masks/definitions/box-like.ts +++ b/apps/web/src/masks/definitions/box-like.ts @@ -16,7 +16,7 @@ import { getBoxMaskHandlePositions, getBoxMaskOverlays, } from "@/masks/handle-positions"; -import { snapMaskInteraction } from "@/masks/snap"; +import { snapBoxMaskInteraction } from "@/masks/snap"; const PERCENTAGE_DISPLAY: Pick< NumberParamDefinition, @@ -198,7 +198,7 @@ export function buildBoxMaskInteraction({ }; }, snap(args) { - return snapMaskInteraction(args); + return snapBoxMaskInteraction(args); }, }; } diff --git a/apps/web/src/masks/definitions/split.ts b/apps/web/src/masks/definitions/split.ts index d8fdc789..ddd4e452 100644 --- a/apps/web/src/masks/definitions/split.ts +++ b/apps/web/src/masks/definitions/split.ts @@ -9,7 +9,7 @@ import { getLineMaskHandlePositions, getLineMaskOverlay, } from "@/masks/handle-positions"; -import { snapMaskInteraction } from "@/masks/snap"; +import { snapSplitMaskInteraction } from "@/masks/snap"; // cos(π/2) returns ~6e-17 in JS, not 0. Values below this threshold are snapped // to exactly 0 to prevent opposite-sign float noise on canvas corners that lie @@ -216,7 +216,7 @@ export const splitMaskDefinition: MaskDefinition = { }; }, snap(args) { - return snapMaskInteraction(args); + return snapSplitMaskInteraction(args); }, }, buildDefault() { diff --git a/apps/web/src/masks/snap.ts b/apps/web/src/masks/snap.ts index cff62ebb..67a0225d 100644 --- a/apps/web/src/masks/snap.ts +++ b/apps/web/src/masks/snap.ts @@ -6,23 +6,19 @@ import { snapScale, snapScaleAxes, type ScaleEdgePreference, - type SnapLine, } from "@/preview/preview-snap"; -import type { RectangleMaskParams, SplitMaskParams } from "@/masks/types"; +import type { + MaskSnapArgs, + MaskSnapResult, + RectangleMaskParams, + SplitMaskParams, +} from "@/masks/types"; import { - isRectangleMaskParams, getMaskSnapGeometry, setMaskLocalCenter, toGlobalMaskSnapLines, } from "./geometry"; -type SharedMaskParams = SplitMaskParams | RectangleMaskParams; - -type MaskSnapResult = { - params: TParams; - activeLines: SnapLine[]; -}; - const CORNER_SIZE_HANDLES = new Set([ "top-left", "top-right", @@ -75,11 +71,33 @@ function snapMaskPosition({ canvasSize, snapThreshold, }: { - proposedParams: SharedMaskParams; + proposedParams: RectangleMaskParams; bounds: ElementBounds; canvasSize: { width: number; height: number }; snapThreshold: { x: number; y: number }; -}): MaskSnapResult { +}): MaskSnapResult; +function snapMaskPosition({ + proposedParams, + bounds, + canvasSize, + snapThreshold, +}: { + proposedParams: SplitMaskParams; + bounds: ElementBounds; + canvasSize: { width: number; height: number }; + snapThreshold: { x: number; y: number }; +}): MaskSnapResult; +function snapMaskPosition({ + proposedParams, + bounds, + canvasSize, + snapThreshold, +}: { + proposedParams: RectangleMaskParams | SplitMaskParams; + bounds: ElementBounds; + canvasSize: { width: number; height: number }; + snapThreshold: { x: number; y: number }; +}): MaskSnapResult { const geometry = getMaskSnapGeometry({ params: proposedParams, bounds, @@ -115,12 +133,18 @@ function snapMaskPosition({ function snapMaskRotation({ proposedParams, }: { - proposedParams: SharedMaskParams; -}): MaskSnapResult { - if (typeof proposedParams.rotation !== "number") { - return { params: proposedParams, activeLines: [] }; - } - + proposedParams: RectangleMaskParams; +}): MaskSnapResult; +function snapMaskRotation({ + proposedParams, +}: { + proposedParams: SplitMaskParams; +}): MaskSnapResult; +function snapMaskRotation({ + proposedParams, +}: { + proposedParams: RectangleMaskParams | SplitMaskParams; +}): MaskSnapResult { const { snappedRotation } = snapRotation({ proposedRotation: proposedParams.rotation, }); @@ -143,19 +167,12 @@ function snapBoxMaskSize({ snapThreshold, }: { handleId: string; - startParams: SharedMaskParams; - proposedParams: SharedMaskParams; + startParams: RectangleMaskParams; + proposedParams: RectangleMaskParams; bounds: ElementBounds; canvasSize: { width: number; height: number }; snapThreshold: { x: number; y: number }; -}): MaskSnapResult { - if ( - !isRectangleMaskParams(startParams) || - !isRectangleMaskParams(proposedParams) - ) { - return { params: proposedParams, activeLines: [] }; - } - +}): MaskSnapResult { const geometry = getMaskSnapGeometry({ params: proposedParams, bounds, @@ -297,7 +314,7 @@ function snapBoxMaskSize({ return { params: proposedParams, activeLines: [] }; } -export function snapMaskInteraction({ +export function snapBoxMaskInteraction({ handleId, startParams, proposedParams, @@ -306,23 +323,23 @@ export function snapMaskInteraction({ snapThreshold, }: { handleId: string; - startParams: TParams; - proposedParams: TParams; + startParams: RectangleMaskParams; + proposedParams: RectangleMaskParams; bounds: ElementBounds; canvasSize: { width: number; height: number }; snapThreshold: { x: number; y: number }; -}): MaskSnapResult { +}): MaskSnapResult { if (handleId === "position") { return snapMaskPosition({ proposedParams, bounds, canvasSize, snapThreshold, - }) as MaskSnapResult; + }); } if (handleId === "rotation") { - return snapMaskRotation({ proposedParams }) as MaskSnapResult; + return snapMaskRotation({ proposedParams }); } return snapBoxMaskSize({ @@ -332,5 +349,28 @@ export function snapMaskInteraction({ bounds, canvasSize, snapThreshold, - }) as MaskSnapResult; + }); +} + +export function snapSplitMaskInteraction({ + handleId, + proposedParams, + bounds, + canvasSize, + snapThreshold, +}: MaskSnapArgs): MaskSnapResult { + if (handleId === "position") { + return snapMaskPosition({ + proposedParams, + bounds, + canvasSize, + snapThreshold, + }); + } + + if (handleId === "rotation") { + return snapMaskRotation({ proposedParams }); + } + + return { params: proposedParams, activeLines: [] }; }