summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/masks
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/lib/masks')
-rw-r--r--apps/web/src/lib/masks/__tests__/snap.test.ts472
1 files changed, 237 insertions, 235 deletions
diff --git a/apps/web/src/lib/masks/__tests__/snap.test.ts b/apps/web/src/lib/masks/__tests__/snap.test.ts
index 41093c88..72b2ca80 100644
--- a/apps/web/src/lib/masks/__tests__/snap.test.ts
+++ b/apps/web/src/lib/masks/__tests__/snap.test.ts
@@ -1,235 +1,237 @@
-import { describe, expect, test } from "bun:test";
-import { getSplitMaskStrokeSegment } from "@/lib/masks/definitions/split";
-import { getMaskSnapGeometry } from "@/lib/masks/geometry";
-import { snapMaskInteraction } from "@/lib/masks/snap";
-import type { ElementBounds } from "@/lib/preview/element-bounds";
-import type { RectangleMaskParams, SplitMaskParams } from "@/lib/masks/types";
-
-const bounds: ElementBounds = {
- cx: 200,
- cy: 150,
- width: 200,
- height: 100,
- rotation: 0,
-};
-
-const canvasSize = {
- width: 400,
- height: 300,
-};
-
-const snapThreshold = {
- x: 8,
- y: 8,
-};
-
-function buildSplitParams(
- overrides: Partial<SplitMaskParams> = {},
-): SplitMaskParams {
- return {
- feather: 0,
- inverted: false,
- strokeColor: "#ffffff",
- strokeWidth: 0,
- centerX: 0,
- centerY: 0,
- rotation: 0,
- ...overrides,
- };
-}
-
-function buildRectangleParams(
- overrides: Partial<RectangleMaskParams> = {},
-): RectangleMaskParams {
- return {
- feather: 0,
- inverted: false,
- strokeColor: "#ffffff",
- strokeWidth: 0,
- centerX: 0,
- centerY: 0,
- width: 0.4,
- height: 0.2,
- rotation: 0,
- scale: 1,
- ...overrides,
- };
-}
-
-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 },
- ];
-}
-
-describe("mask geometry", () => {
- test("resolves split mask center from centerX and centerY", () => {
- expect(
- getMaskSnapGeometry({
- params: buildSplitParams({
- centerX: 0.25,
- centerY: -0.5,
- rotation: 45,
- }),
- bounds,
- }),
- ).toEqual({
- position: { x: 50, y: -50 },
- size: { width: 0, height: 0 },
- rotation: 45,
- });
- });
-
- test("resolves box mask center and size from centerX and centerY", () => {
- expect(
- getMaskSnapGeometry({
- params: buildRectangleParams({
- centerX: -0.25,
- centerY: 0.5,
- width: 0.5,
- height: 0.6,
- rotation: 30,
- }),
- bounds,
- }),
- ).toEqual({
- position: { x: -50, y: 50 },
- size: { width: 100, height: 60 },
- rotation: 30,
- });
- });
-
- test("returns a vertical split stroke segment for rotation 0", () => {
- const segment = getSplitMaskStrokeSegment({
- resolvedParams: buildSplitParams(),
- width: bounds.width,
- height: bounds.height,
- });
-
- expect(segment).not.toBeNull();
- if (!segment) {
- throw new Error("Expected split stroke segment for rotation 0");
- }
- expect(sortSegment(segment)).toEqual([
- { x: bounds.width / 2, y: 0 },
- { x: bounds.width / 2, y: bounds.height },
- ]);
- });
-
- test("returns a horizontal split stroke segment for rotation 90", () => {
- const segment = getSplitMaskStrokeSegment({
- resolvedParams: buildSplitParams({ rotation: 90 }),
- width: bounds.width,
- height: bounds.height,
- });
-
- expect(segment).not.toBeNull();
- if (!segment) {
- throw new Error("Expected split stroke segment for rotation 90");
- }
- expect(sortSegment(segment)).toEqual([
- { x: 0, y: bounds.height / 2 },
- { x: bounds.width, y: bounds.height / 2 },
- ]);
- });
-});
-
-describe("mask snapping", () => {
- test("snaps split mask movement using the shared position pipeline", () => {
- const result = snapMaskInteraction({
- handleId: "position",
- startParams: buildSplitParams({
- centerX: 0.03,
- centerY: -0.04,
- }),
- proposedParams: buildSplitParams({
- centerX: 0.03,
- centerY: -0.04,
- }),
- bounds,
- canvasSize,
- snapThreshold,
- });
-
- expect(result.params.centerX).toBe(0);
- expect(result.params.centerY).toBe(0);
- expect(result.activeLines).toEqual([
- { type: "vertical", position: 0 },
- { type: "horizontal", position: 0 },
- ]);
- });
-
- test("snaps box mask movement against element center and edges", () => {
- const result = snapMaskInteraction({
- handleId: "position",
- startParams: buildRectangleParams(),
- proposedParams: buildRectangleParams({
- centerX: 0.29,
- centerY: 0.03,
- }),
- bounds,
- canvasSize,
- snapThreshold,
- });
-
- expect(result.params.centerX).toBeCloseTo(0.3);
- expect(result.params.centerY).toBe(0);
- expect(result.activeLines).toEqual([
- { type: "vertical", position: 100 },
- { type: "horizontal", position: 0 },
- ]);
- });
-
- test("snaps mask rotation through the shared rotation path", () => {
- const result = snapMaskInteraction({
- handleId: "rotation",
- startParams: buildRectangleParams(),
- proposedParams: buildRectangleParams({
- rotation: 88,
- }),
- bounds,
- canvasSize,
- snapThreshold,
- });
-
- expect(result.params.rotation).toBe(90);
- expect(result.activeLines).toEqual([]);
- });
-
- test("snaps edge resize for box masks", () => {
- const result = snapMaskInteraction({
- handleId: "right",
- startParams: buildRectangleParams(),
- proposedParams: buildRectangleParams({
- width: 0.98,
- }),
- bounds,
- canvasSize,
- snapThreshold,
- });
-
- expect(result.params.width).toBe(1);
- expect(result.activeLines).toEqual([{ type: "vertical", position: 100 }]);
- });
-
- test("snaps corner resize for box masks", () => {
- const result = snapMaskInteraction({
- handleId: "bottom-right",
- startParams: buildRectangleParams(),
- proposedParams: buildRectangleParams({
- width: 0.99,
- height: 0.495,
- }),
- bounds,
- canvasSize,
- snapThreshold,
- });
-
- expect(result.params.width).toBe(1);
- expect(result.params.height).toBe(0.5);
- expect(result.activeLines).toEqual([{ type: "vertical", position: 100 }]);
- });
-});
+import { describe, expect, test } from "bun:test";
+import { getSplitMaskStrokeSegment } from "@/lib/masks/definitions/split";
+import { getMaskSnapGeometry } from "@/lib/masks/geometry";
+import { snapMaskInteraction } from "@/lib/masks/snap";
+import type { ElementBounds } from "@/lib/preview/element-bounds";
+import type { RectangleMaskParams, SplitMaskParams } from "@/lib/masks/types";
+
+const bounds: ElementBounds = {
+ cx: 200,
+ cy: 150,
+ width: 200,
+ height: 100,
+ rotation: 0,
+};
+
+const canvasSize = {
+ width: 400,
+ height: 300,
+};
+
+const snapThreshold = {
+ x: 8,
+ y: 8,
+};
+
+function buildSplitParams(
+ overrides: Partial<SplitMaskParams> = {},
+): SplitMaskParams {
+ return {
+ feather: 0,
+ inverted: false,
+ strokeColor: "#ffffff",
+ strokeWidth: 0,
+ strokeAlign: "center",
+ centerX: 0,
+ centerY: 0,
+ rotation: 0,
+ ...overrides,
+ };
+}
+
+function buildRectangleParams(
+ overrides: Partial<RectangleMaskParams> = {},
+): RectangleMaskParams {
+ return {
+ feather: 0,
+ inverted: false,
+ strokeColor: "#ffffff",
+ strokeWidth: 0,
+ strokeAlign: "center",
+ centerX: 0,
+ centerY: 0,
+ width: 0.4,
+ height: 0.2,
+ rotation: 0,
+ scale: 1,
+ ...overrides,
+ };
+}
+
+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 },
+ ];
+}
+
+describe("mask geometry", () => {
+ test("resolves split mask center from centerX and centerY", () => {
+ expect(
+ getMaskSnapGeometry({
+ params: buildSplitParams({
+ centerX: 0.25,
+ centerY: -0.5,
+ rotation: 45,
+ }),
+ bounds,
+ }),
+ ).toEqual({
+ position: { x: 50, y: -50 },
+ size: { width: 0, height: 0 },
+ rotation: 45,
+ });
+ });
+
+ test("resolves box mask center and size from centerX and centerY", () => {
+ expect(
+ getMaskSnapGeometry({
+ params: buildRectangleParams({
+ centerX: -0.25,
+ centerY: 0.5,
+ width: 0.5,
+ height: 0.6,
+ rotation: 30,
+ }),
+ bounds,
+ }),
+ ).toEqual({
+ position: { x: -50, y: 50 },
+ size: { width: 100, height: 60 },
+ rotation: 30,
+ });
+ });
+
+ test("returns a vertical split stroke segment for rotation 0", () => {
+ const segment = getSplitMaskStrokeSegment({
+ resolvedParams: buildSplitParams(),
+ width: bounds.width,
+ height: bounds.height,
+ });
+
+ expect(segment).not.toBeNull();
+ if (!segment) {
+ throw new Error("Expected split stroke segment for rotation 0");
+ }
+ expect(sortSegment(segment)).toEqual([
+ { x: bounds.width / 2, y: 0 },
+ { x: bounds.width / 2, y: bounds.height },
+ ]);
+ });
+
+ test("returns a horizontal split stroke segment for rotation 90", () => {
+ const segment = getSplitMaskStrokeSegment({
+ resolvedParams: buildSplitParams({ rotation: 90 }),
+ width: bounds.width,
+ height: bounds.height,
+ });
+
+ expect(segment).not.toBeNull();
+ if (!segment) {
+ throw new Error("Expected split stroke segment for rotation 90");
+ }
+ expect(sortSegment(segment)).toEqual([
+ { x: 0, y: bounds.height / 2 },
+ { x: bounds.width, y: bounds.height / 2 },
+ ]);
+ });
+});
+
+describe("mask snapping", () => {
+ test("snaps split mask movement using the shared position pipeline", () => {
+ const result = snapMaskInteraction({
+ handleId: "position",
+ startParams: buildSplitParams({
+ centerX: 0.03,
+ centerY: -0.04,
+ }),
+ proposedParams: buildSplitParams({
+ centerX: 0.03,
+ centerY: -0.04,
+ }),
+ bounds,
+ canvasSize,
+ snapThreshold,
+ });
+
+ expect(result.params.centerX).toBe(0);
+ expect(result.params.centerY).toBe(0);
+ expect(result.activeLines).toEqual([
+ { type: "vertical", position: 0 },
+ { type: "horizontal", position: 0 },
+ ]);
+ });
+
+ test("snaps box mask movement against element center and edges", () => {
+ const result = snapMaskInteraction({
+ handleId: "position",
+ startParams: buildRectangleParams(),
+ proposedParams: buildRectangleParams({
+ centerX: 0.29,
+ centerY: 0.03,
+ }),
+ bounds,
+ canvasSize,
+ snapThreshold,
+ });
+
+ expect(result.params.centerX).toBeCloseTo(0.3);
+ expect(result.params.centerY).toBe(0);
+ expect(result.activeLines).toEqual([
+ { type: "vertical", position: 100 },
+ { type: "horizontal", position: 0 },
+ ]);
+ });
+
+ test("snaps mask rotation through the shared rotation path", () => {
+ const result = snapMaskInteraction({
+ handleId: "rotation",
+ startParams: buildRectangleParams(),
+ proposedParams: buildRectangleParams({
+ rotation: 88,
+ }),
+ bounds,
+ canvasSize,
+ snapThreshold,
+ });
+
+ expect(result.params.rotation).toBe(90);
+ expect(result.activeLines).toEqual([]);
+ });
+
+ test("snaps edge resize for box masks", () => {
+ const result = snapMaskInteraction({
+ handleId: "right",
+ startParams: buildRectangleParams(),
+ proposedParams: buildRectangleParams({
+ width: 0.98,
+ }),
+ bounds,
+ canvasSize,
+ snapThreshold,
+ });
+
+ expect(result.params.width).toBe(1);
+ expect(result.activeLines).toEqual([{ type: "vertical", position: 100 }]);
+ });
+
+ test("snaps corner resize for box masks", () => {
+ const result = snapMaskInteraction({
+ handleId: "bottom-right",
+ startParams: buildRectangleParams(),
+ proposedParams: buildRectangleParams({
+ width: 0.99,
+ height: 0.495,
+ }),
+ bounds,
+ canvasSize,
+ snapThreshold,
+ });
+
+ expect(result.params.width).toBe(1);
+ expect(result.params.height).toBe(0.5);
+ expect(result.activeLines).toEqual([{ type: "vertical", position: 100 }]);
+ });
+});