diff options
Diffstat (limited to 'apps/web/src/masks/__tests__/snap.test.ts')
| -rw-r--r-- | apps/web/src/masks/__tests__/snap.test.ts | 130 |
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, |
