summaryrefslogtreecommitdiff
path: root/apps/web/src/masks/__tests__/snap.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/masks/__tests__/snap.test.ts')
-rw-r--r--apps/web/src/masks/__tests__/snap.test.ts130
1 files changed, 83 insertions, 47 deletions
diff --git a/apps/web/src/masks/__tests__/snap.test.ts b/apps/web/src/masks/__tests__/snap.test.ts
index 2f33615f..a5d0ceb3 100644
--- a/apps/web/src/masks/__tests__/snap.test.ts
+++ b/apps/web/src/masks/__tests__/snap.test.ts
@@ -1,22 +1,22 @@
import { describe, expect, test } from "bun:test";
import {
- findClosestPointOnCustomMaskSegment,
- getCustomMaskClosedStateAfterPointRemoval,
- insertPointIntoCustomMaskSegment,
- removeCustomMaskPoints,
-} from "@/masks/custom-path";
+ findClosestPointOnFreeformSegment,
+ getFreeformPathClosedStateAfterPointRemoval,
+ insertPointIntoFreeformSegment,
+ removeFreeformPathPoints,
+} from "@/masks/freeform/path";
import {
- appendPointToCustomMask,
- customMaskDefinition,
- insertPointOnCustomMaskSegment,
-} from "@/masks/definitions/custom";
-import { getSplitMaskStrokeSegment } from "@/masks/definitions/split";
-import { textMaskDefinition } from "@/masks/definitions/text";
+ appendPointToFreeformPathMask,
+ freeformMaskDefinition,
+ insertPointOnFreeformSegment,
+} from "@/masks/freeform/definition";
+import { getSplitMaskStrokeSegment } from "@/masks/builtin/definitions/split";
+import { textMaskDefinition } from "@/masks/builtin/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,
+ FreeformPathMaskParams,
RectangleMaskParams,
SplitMaskParams,
TextMaskParams,
@@ -100,9 +100,9 @@ function buildTextMaskParams(
};
}
-function buildCustomMaskParams(
- overrides: Partial<CustomMaskParams> = {},
-): CustomMaskParams {
+function buildFreeformPathMaskParams(
+ overrides: Partial<FreeformPathMaskParams> = {},
+): FreeformPathMaskParams {
return {
feather: 0,
inverted: false,
@@ -150,10 +150,10 @@ function buildCustomMaskParams(
function sortSegment(
segment: [{ x: number; y: number }, { x: number; y: number }],
): [{ x: number; y: number }, { x: number; y: number }] {
- return [...segment].sort((a, b) => (a.x === b.x ? a.y - b.y : a.x - b.x)) as [
- { x: number; y: number },
- { x: number; y: number },
- ];
+ const [first, second] = segment;
+ return first.x < second.x || (first.x === second.x && first.y <= second.y)
+ ? [first, second]
+ : [second, first];
}
describe("mask geometry", () => {
@@ -230,8 +230,8 @@ describe("mask geometry", () => {
describe("mask snapping", () => {
test("snaps split mask movement using the shared position pipeline", () => {
- const result = snapMaskInteraction({
- handleId: "position",
+ const result = snapSplitMaskInteraction({
+ handleId: { kind: "position" },
startParams: buildSplitParams({
centerX: 0.03,
centerY: -0.04,
@@ -254,8 +254,8 @@ describe("mask snapping", () => {
});
test("snaps box mask movement against element center and edges", () => {
- const result = snapMaskInteraction({
- handleId: "position",
+ const result = snapBoxMaskInteraction({
+ handleId: { kind: "position" },
startParams: buildRectangleParams(),
proposedParams: buildRectangleParams({
centerX: 0.29,
@@ -275,8 +275,8 @@ describe("mask snapping", () => {
});
test("snaps mask rotation through the shared rotation path", () => {
- const result = snapMaskInteraction({
- handleId: "rotation",
+ const result = snapBoxMaskInteraction({
+ handleId: { kind: "rotation" },
startParams: buildRectangleParams(),
proposedParams: buildRectangleParams({
rotation: 88,
@@ -291,8 +291,8 @@ describe("mask snapping", () => {
});
test("snaps edge resize for box masks", () => {
- const result = snapMaskInteraction({
- handleId: "right",
+ const result = snapBoxMaskInteraction({
+ handleId: { kind: "edge", side: "right" },
startParams: buildRectangleParams(),
proposedParams: buildRectangleParams({
width: 0.98,
@@ -307,8 +307,8 @@ describe("mask snapping", () => {
});
test("snaps corner resize for box masks", () => {
- const result = snapMaskInteraction({
- handleId: "bottom-right",
+ const result = snapBoxMaskInteraction({
+ handleId: { kind: "corner", corner: { x: "right", y: "bottom" } },
startParams: buildRectangleParams(),
proposedParams: buildRectangleParams({
width: 0.99,
@@ -324,13 +324,49 @@ describe("mask snapping", () => {
expect(result.activeLines).toEqual([{ type: "vertical", position: 100 }]);
});
+ test("snaps vertical edge resize for box masks", () => {
+ // bounds.height=100 → localCanvasSize.height/2=50 is the bottom snap target.
+ // height=0.2 → baseHeight=20 → aabbHalfH=10.
+ // At height=0.98 → proposedScaleY=4.9 → bottomEdge=0+10*4.9=49.
+ // |49-50|=1 < threshold(8) → snaps to height=0.2*5=1.0; line at position 50.
+ const result = snapBoxMaskInteraction({
+ handleId: { kind: "edge", side: "bottom" },
+ startParams: buildRectangleParams(),
+ proposedParams: buildRectangleParams({ height: 0.98 }),
+ bounds,
+ canvasSize,
+ snapThreshold,
+ });
+
+ expect(result.params.height).toBe(1);
+ expect(result.activeLines).toEqual([{ type: "horizontal", position: 50 }]);
+ });
+
+ test("snaps uniform scale handle for box masks", () => {
+ // bounds.width=200 → localCanvasSize.width/2=100 is the right snap target.
+ // width=0.4, scale=1 → aabbHalfW = (0.4*200)/2 * 1 = 40.
+ // At scale=2.48 → rightEdge=0+40*2.48=99.2; |99.2-100|=0.8 < threshold(8)
+ // → snaps to scale=1*(100/40)=2.5; line at position 100.
+ const result = snapBoxMaskInteraction({
+ handleId: { kind: "scale" },
+ startParams: buildRectangleParams({ scale: 1 }),
+ proposedParams: buildRectangleParams({ scale: 2.48 }),
+ bounds,
+ canvasSize,
+ snapThreshold,
+ });
+
+ expect(result.params.scale).toBe(2.5);
+ expect(result.activeLines).toEqual([{ type: "vertical", position: 100 }]);
+ });
+
test("snaps text mask movement using intrinsic text bounds", () => {
const params = buildTextMaskParams({
centerX: 0.03,
centerY: -0.04,
});
const result = textMaskDefinition.interaction.snap?.({
- handleId: "position",
+ handleId: { kind: "position" },
startParams: params,
proposedParams: params,
bounds,
@@ -347,12 +383,12 @@ describe("mask snapping", () => {
});
test("snaps custom mask movement using path geometry bounds", () => {
- const params = buildCustomMaskParams({
+ const params = buildFreeformPathMaskParams({
centerX: 0.03,
centerY: -0.04,
});
- const result = customMaskDefinition.interaction.snap?.({
- handleId: "position",
+ const result = freeformMaskDefinition.interaction.snap?.({
+ handleId: { kind: "position" },
startParams: params,
proposedParams: params,
bounds,
@@ -381,11 +417,11 @@ describe("mask snapping", () => {
describe("custom mask creation", () => {
test("anchors the first point at the click position", () => {
- const params = buildCustomMaskParams({
+ const params = buildFreeformPathMaskParams({
path: [],
closed: false,
});
- const next = appendPointToCustomMask({
+ const next = appendPointToFreeformPathMask({
params,
canvasPoint: { x: bounds.cx + 20, y: bounds.cy - 10 },
bounds,
@@ -401,8 +437,8 @@ describe("custom mask creation", () => {
describe("custom mask point deletion", () => {
test("removes the selected points by id", () => {
- const points = buildCustomMaskParams().path;
- const nextPoints = removeCustomMaskPoints({
+ const points = buildFreeformPathMaskParams().path;
+ const nextPoints = removeFreeformPathPoints({
points,
pointIds: ["b"],
});
@@ -411,14 +447,14 @@ describe("custom mask point deletion", () => {
});
test("reopens a closed path once fewer than three points remain", () => {
- const points = buildCustomMaskParams().path;
- const nextPoints = removeCustomMaskPoints({
+ const points = buildFreeformPathMaskParams().path;
+ const nextPoints = removeFreeformPathPoints({
points,
pointIds: ["c"],
});
expect(
- getCustomMaskClosedStateAfterPointRemoval({
+ getFreeformPathClosedStateAfterPointRemoval({
wasClosed: true,
remainingPointCount: nextPoints.length,
}),
@@ -428,9 +464,9 @@ describe("custom mask point deletion", () => {
describe("custom mask point insertion", () => {
test("finds the closest point on the clicked segment", () => {
- const params = buildCustomMaskParams();
+ const params = buildFreeformPathMaskParams();
const points = params.path;
- const closestPoint = findClosestPointOnCustomMaskSegment({
+ const closestPoint = findClosestPointOnFreeformSegment({
points,
segmentIndex: 0,
canvasPoint: { x: bounds.cx, y: bounds.cy - 10 },
@@ -449,8 +485,8 @@ describe("custom mask point insertion", () => {
});
test("splits a segment into two segments at the insertion point", () => {
- const points = buildCustomMaskParams().path;
- const nextPoints = insertPointIntoCustomMaskSegment({
+ const points = buildFreeformPathMaskParams().path;
+ const nextPoints = insertPointIntoFreeformSegment({
points,
segmentIndex: 0,
pointId: "new",
@@ -471,8 +507,8 @@ describe("custom mask point insertion", () => {
});
test("builds updated custom mask params for a clicked segment", () => {
- const result = insertPointOnCustomMaskSegment({
- params: buildCustomMaskParams(),
+ const result = insertPointOnFreeformSegment({
+ params: buildFreeformPathMaskParams(),
segmentIndex: 0,
canvasPoint: { x: bounds.cx, y: bounds.cy - 10 },
bounds,