refactor: split mask snap dispatch

Made-with: Cursor
This commit is contained in:
Maze Winther 2026-04-29 18:23:08 +02:00
parent 401cc35743
commit 6ea5a156b6
4 changed files with 85 additions and 45 deletions

View File

@ -13,7 +13,7 @@ import {
import { getSplitMaskStrokeSegment } from "@/masks/definitions/split"; import { getSplitMaskStrokeSegment } from "@/masks/definitions/split";
import { textMaskDefinition } from "@/masks/definitions/text"; import { textMaskDefinition } from "@/masks/definitions/text";
import { getMaskSnapGeometry } from "@/masks/geometry"; 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 { ElementBounds } from "@/preview/element-bounds";
import type { import type {
CustomMaskParams, CustomMaskParams,
@ -230,7 +230,7 @@ describe("mask geometry", () => {
describe("mask snapping", () => { describe("mask snapping", () => {
test("snaps split mask movement using the shared position pipeline", () => { test("snaps split mask movement using the shared position pipeline", () => {
const result = snapMaskInteraction({ const result = snapSplitMaskInteraction({
handleId: "position", handleId: "position",
startParams: buildSplitParams({ startParams: buildSplitParams({
centerX: 0.03, centerX: 0.03,
@ -254,7 +254,7 @@ describe("mask snapping", () => {
}); });
test("snaps box mask movement against element center and edges", () => { test("snaps box mask movement against element center and edges", () => {
const result = snapMaskInteraction({ const result = snapBoxMaskInteraction({
handleId: "position", handleId: "position",
startParams: buildRectangleParams(), startParams: buildRectangleParams(),
proposedParams: buildRectangleParams({ proposedParams: buildRectangleParams({
@ -275,7 +275,7 @@ describe("mask snapping", () => {
}); });
test("snaps mask rotation through the shared rotation path", () => { test("snaps mask rotation through the shared rotation path", () => {
const result = snapMaskInteraction({ const result = snapBoxMaskInteraction({
handleId: "rotation", handleId: "rotation",
startParams: buildRectangleParams(), startParams: buildRectangleParams(),
proposedParams: buildRectangleParams({ proposedParams: buildRectangleParams({
@ -291,7 +291,7 @@ describe("mask snapping", () => {
}); });
test("snaps edge resize for box masks", () => { test("snaps edge resize for box masks", () => {
const result = snapMaskInteraction({ const result = snapBoxMaskInteraction({
handleId: "right", handleId: "right",
startParams: buildRectangleParams(), startParams: buildRectangleParams(),
proposedParams: buildRectangleParams({ proposedParams: buildRectangleParams({
@ -307,7 +307,7 @@ describe("mask snapping", () => {
}); });
test("snaps corner resize for box masks", () => { test("snaps corner resize for box masks", () => {
const result = snapMaskInteraction({ const result = snapBoxMaskInteraction({
handleId: "bottom-right", handleId: "bottom-right",
startParams: buildRectangleParams(), startParams: buildRectangleParams(),
proposedParams: buildRectangleParams({ proposedParams: buildRectangleParams({

View File

@ -16,7 +16,7 @@ import {
getBoxMaskHandlePositions, getBoxMaskHandlePositions,
getBoxMaskOverlays, getBoxMaskOverlays,
} from "@/masks/handle-positions"; } from "@/masks/handle-positions";
import { snapMaskInteraction } from "@/masks/snap"; import { snapBoxMaskInteraction } from "@/masks/snap";
const PERCENTAGE_DISPLAY: Pick< const PERCENTAGE_DISPLAY: Pick<
NumberParamDefinition, NumberParamDefinition,
@ -198,7 +198,7 @@ export function buildBoxMaskInteraction({
}; };
}, },
snap(args) { snap(args) {
return snapMaskInteraction(args); return snapBoxMaskInteraction(args);
}, },
}; };
} }

View File

@ -9,7 +9,7 @@ import {
getLineMaskHandlePositions, getLineMaskHandlePositions,
getLineMaskOverlay, getLineMaskOverlay,
} from "@/masks/handle-positions"; } 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 // 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 // to exactly 0 to prevent opposite-sign float noise on canvas corners that lie
@ -216,7 +216,7 @@ export const splitMaskDefinition: MaskDefinition<SplitMaskParams> = {
}; };
}, },
snap(args) { snap(args) {
return snapMaskInteraction(args); return snapSplitMaskInteraction(args);
}, },
}, },
buildDefault() { buildDefault() {

View File

@ -6,23 +6,19 @@ import {
snapScale, snapScale,
snapScaleAxes, snapScaleAxes,
type ScaleEdgePreference, type ScaleEdgePreference,
type SnapLine,
} from "@/preview/preview-snap"; } from "@/preview/preview-snap";
import type { RectangleMaskParams, SplitMaskParams } from "@/masks/types"; import type {
MaskSnapArgs,
MaskSnapResult,
RectangleMaskParams,
SplitMaskParams,
} from "@/masks/types";
import { import {
isRectangleMaskParams,
getMaskSnapGeometry, getMaskSnapGeometry,
setMaskLocalCenter, setMaskLocalCenter,
toGlobalMaskSnapLines, toGlobalMaskSnapLines,
} from "./geometry"; } from "./geometry";
type SharedMaskParams = SplitMaskParams | RectangleMaskParams;
type MaskSnapResult<TParams extends SharedMaskParams> = {
params: TParams;
activeLines: SnapLine[];
};
const CORNER_SIZE_HANDLES = new Set([ const CORNER_SIZE_HANDLES = new Set([
"top-left", "top-left",
"top-right", "top-right",
@ -75,11 +71,33 @@ function snapMaskPosition({
canvasSize, canvasSize,
snapThreshold, snapThreshold,
}: { }: {
proposedParams: SharedMaskParams; proposedParams: RectangleMaskParams;
bounds: ElementBounds; bounds: ElementBounds;
canvasSize: { width: number; height: number }; canvasSize: { width: number; height: number };
snapThreshold: { x: number; y: number }; snapThreshold: { x: number; y: number };
}): MaskSnapResult<SharedMaskParams> { }): MaskSnapResult<RectangleMaskParams>;
function snapMaskPosition({
proposedParams,
bounds,
canvasSize,
snapThreshold,
}: {
proposedParams: SplitMaskParams;
bounds: ElementBounds;
canvasSize: { width: number; height: number };
snapThreshold: { x: number; y: number };
}): MaskSnapResult<SplitMaskParams>;
function snapMaskPosition({
proposedParams,
bounds,
canvasSize,
snapThreshold,
}: {
proposedParams: RectangleMaskParams | SplitMaskParams;
bounds: ElementBounds;
canvasSize: { width: number; height: number };
snapThreshold: { x: number; y: number };
}): MaskSnapResult<RectangleMaskParams | SplitMaskParams> {
const geometry = getMaskSnapGeometry({ const geometry = getMaskSnapGeometry({
params: proposedParams, params: proposedParams,
bounds, bounds,
@ -115,12 +133,18 @@ function snapMaskPosition({
function snapMaskRotation({ function snapMaskRotation({
proposedParams, proposedParams,
}: { }: {
proposedParams: SharedMaskParams; proposedParams: RectangleMaskParams;
}): MaskSnapResult<SharedMaskParams> { }): MaskSnapResult<RectangleMaskParams>;
if (typeof proposedParams.rotation !== "number") { function snapMaskRotation({
return { params: proposedParams, activeLines: [] }; proposedParams,
} }: {
proposedParams: SplitMaskParams;
}): MaskSnapResult<SplitMaskParams>;
function snapMaskRotation({
proposedParams,
}: {
proposedParams: RectangleMaskParams | SplitMaskParams;
}): MaskSnapResult<RectangleMaskParams | SplitMaskParams> {
const { snappedRotation } = snapRotation({ const { snappedRotation } = snapRotation({
proposedRotation: proposedParams.rotation, proposedRotation: proposedParams.rotation,
}); });
@ -143,19 +167,12 @@ function snapBoxMaskSize({
snapThreshold, snapThreshold,
}: { }: {
handleId: string; handleId: string;
startParams: SharedMaskParams; startParams: RectangleMaskParams;
proposedParams: SharedMaskParams; proposedParams: RectangleMaskParams;
bounds: ElementBounds; bounds: ElementBounds;
canvasSize: { width: number; height: number }; canvasSize: { width: number; height: number };
snapThreshold: { x: number; y: number }; snapThreshold: { x: number; y: number };
}): MaskSnapResult<SharedMaskParams> { }): MaskSnapResult<RectangleMaskParams> {
if (
!isRectangleMaskParams(startParams) ||
!isRectangleMaskParams(proposedParams)
) {
return { params: proposedParams, activeLines: [] };
}
const geometry = getMaskSnapGeometry({ const geometry = getMaskSnapGeometry({
params: proposedParams, params: proposedParams,
bounds, bounds,
@ -297,7 +314,7 @@ function snapBoxMaskSize({
return { params: proposedParams, activeLines: [] }; return { params: proposedParams, activeLines: [] };
} }
export function snapMaskInteraction<TParams extends SharedMaskParams>({ export function snapBoxMaskInteraction({
handleId, handleId,
startParams, startParams,
proposedParams, proposedParams,
@ -306,23 +323,23 @@ export function snapMaskInteraction<TParams extends SharedMaskParams>({
snapThreshold, snapThreshold,
}: { }: {
handleId: string; handleId: string;
startParams: TParams; startParams: RectangleMaskParams;
proposedParams: TParams; proposedParams: RectangleMaskParams;
bounds: ElementBounds; bounds: ElementBounds;
canvasSize: { width: number; height: number }; canvasSize: { width: number; height: number };
snapThreshold: { x: number; y: number }; snapThreshold: { x: number; y: number };
}): MaskSnapResult<TParams> { }): MaskSnapResult<RectangleMaskParams> {
if (handleId === "position") { if (handleId === "position") {
return snapMaskPosition({ return snapMaskPosition({
proposedParams, proposedParams,
bounds, bounds,
canvasSize, canvasSize,
snapThreshold, snapThreshold,
}) as MaskSnapResult<TParams>; });
} }
if (handleId === "rotation") { if (handleId === "rotation") {
return snapMaskRotation({ proposedParams }) as MaskSnapResult<TParams>; return snapMaskRotation({ proposedParams });
} }
return snapBoxMaskSize({ return snapBoxMaskSize({
@ -332,5 +349,28 @@ export function snapMaskInteraction<TParams extends SharedMaskParams>({
bounds, bounds,
canvasSize, canvasSize,
snapThreshold, snapThreshold,
}) as MaskSnapResult<TParams>; });
}
export function snapSplitMaskInteraction({
handleId,
proposedParams,
bounds,
canvasSize,
snapThreshold,
}: MaskSnapArgs<SplitMaskParams>): MaskSnapResult<SplitMaskParams> {
if (handleId === "position") {
return snapMaskPosition({
proposedParams,
bounds,
canvasSize,
snapThreshold,
});
}
if (handleId === "rotation") {
return snapMaskRotation({ proposedParams });
}
return { params: proposedParams, activeLines: [] };
} }