summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/masks/snap.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-03-27 00:55:15 +0100
committerMaze Winther <mazewinther@gmail.com>2026-03-29 15:56:48 +0200
commit4d127a038bf768eded68aae3cfa5c1b12241bdac (patch)
tree16bbb801caf6770546cff4fdb37b6408ec9ead98 /apps/web/src/lib/masks/snap.ts
parent82817cb50cb0487350573d6129130aec705f924b (diff)
downloadopencut-4d127a038bf768eded68aae3cfa5c1b12241bdac.tar.gz
chore: normalize line endings
Diffstat (limited to 'apps/web/src/lib/masks/snap.ts')
-rw-r--r--apps/web/src/lib/masks/snap.ts670
1 files changed, 335 insertions, 335 deletions
diff --git a/apps/web/src/lib/masks/snap.ts b/apps/web/src/lib/masks/snap.ts
index 83cc0e99..13deb7cd 100644
--- a/apps/web/src/lib/masks/snap.ts
+++ b/apps/web/src/lib/masks/snap.ts
@@ -1,335 +1,335 @@
-import type { ElementBounds } from "@/lib/preview/element-bounds";
-import { MIN_MASK_DIMENSION } from "@/constants/mask-constants";
-import {
- snapPosition,
- snapRotation,
- snapScale,
- snapScaleAxes,
- type ScaleEdgePreference,
- type SnapLine,
-} from "@/lib/preview/preview-snap";
-import type { ParamValues } from "@/lib/params";
-import {
- isRectangleMaskParams,
- getMaskSnapGeometry,
- setMaskLocalCenter,
- toGlobalMaskSnapLines,
-} from "./geometry";
-
-type MaskSnapResult = {
- params: ParamValues;
- activeLines: SnapLine[];
-};
-
-const CORNER_SIZE_HANDLES = new Set([
- "top-left",
- "top-right",
- "bottom-left",
- "bottom-right",
-]);
-
-function getClampedRatio({
- next,
- base,
-}: {
- next: number;
- base: number;
-}): number {
- return (
- Math.max(next, MIN_MASK_DIMENSION) / Math.max(base, MIN_MASK_DIMENSION)
- );
-}
-
-function getPreferredEdges({
- handleId,
-}: {
- handleId: string;
-}): ScaleEdgePreference | undefined {
- const preferredEdges = {
- left:
- handleId === "left" ||
- handleId === "top-left" ||
- handleId === "bottom-left",
- right:
- handleId === "right" ||
- handleId === "top-right" ||
- handleId === "bottom-right",
- top:
- handleId === "top" || handleId === "top-left" || handleId === "top-right",
- bottom:
- handleId === "bottom" ||
- handleId === "bottom-left" ||
- handleId === "bottom-right",
- } satisfies ScaleEdgePreference;
-
- return Object.values(preferredEdges).some(Boolean)
- ? preferredEdges
- : undefined;
-}
-
-function snapMaskPosition({
- proposedParams,
- bounds,
- canvasSize,
- snapThreshold,
-}: {
- proposedParams: ParamValues;
- bounds: ElementBounds;
- canvasSize: { width: number; height: number };
- snapThreshold: { x: number; y: number };
-}): MaskSnapResult {
- const geometry = getMaskSnapGeometry({
- params: proposedParams,
- bounds,
- });
- if (!geometry) {
- return { params: proposedParams, activeLines: [] };
- }
-
- const { snappedPosition, activeLines } = snapPosition({
- proposedPosition: geometry.position,
- canvasSize: bounds,
- elementSize: geometry.size,
- rotation: geometry.rotation,
- snapThreshold,
- });
-
- return {
- params: {
- ...proposedParams,
- ...setMaskLocalCenter({
- center: snappedPosition,
- bounds,
- }),
- },
- activeLines: toGlobalMaskSnapLines({
- lines: activeLines,
- bounds,
- canvasSize,
- }),
- };
-}
-
-function snapMaskRotation({
- proposedParams,
-}: {
- proposedParams: ParamValues;
-}): MaskSnapResult {
- if (typeof proposedParams.rotation !== "number") {
- return { params: proposedParams, activeLines: [] };
- }
-
- const { snappedRotation } = snapRotation({
- proposedRotation: proposedParams.rotation,
- });
-
- return {
- params: {
- ...proposedParams,
- rotation: snappedRotation,
- },
- activeLines: [],
- };
-}
-
-function snapBoxMaskSize({
- handleId,
- startParams,
- proposedParams,
- bounds,
- canvasSize,
- snapThreshold,
-}: {
- handleId: string;
- startParams: ParamValues;
- proposedParams: ParamValues;
- bounds: ElementBounds;
- canvasSize: { width: number; height: number };
- snapThreshold: { x: number; y: number };
-}): MaskSnapResult {
- if (
- !isRectangleMaskParams(startParams) ||
- !isRectangleMaskParams(proposedParams)
- ) {
- return { params: proposedParams, activeLines: [] };
- }
-
- const geometry = getMaskSnapGeometry({
- params: proposedParams,
- bounds,
- });
- if (!geometry) {
- return { params: proposedParams, activeLines: [] };
- }
-
- const localCanvasSize = bounds;
- const baseWidth =
- Math.max(startParams.width, MIN_MASK_DIMENSION) * bounds.width;
- const baseHeight =
- Math.max(startParams.height, MIN_MASK_DIMENSION) * bounds.height;
- const preferredEdges = getPreferredEdges({ handleId });
-
- if (handleId === "right" || handleId === "left") {
- const proposedScaleX = getClampedRatio({
- next: proposedParams.width,
- base: startParams.width,
- });
- const { x } = snapScaleAxes({
- proposedScaleX,
- proposedScaleY: 1,
- position: geometry.position,
- baseWidth,
- baseHeight,
- rotation: proposedParams.rotation,
- canvasSize: localCanvasSize,
- snapThreshold,
- preferredEdges,
- });
-
- return {
- params: {
- ...proposedParams,
- width: Math.max(MIN_MASK_DIMENSION, startParams.width * x.snappedScale),
- },
- activeLines: toGlobalMaskSnapLines({
- lines: x.activeLines,
- bounds,
- canvasSize,
- }),
- };
- }
-
- if (handleId === "top" || handleId === "bottom") {
- const proposedScaleY = getClampedRatio({
- next: proposedParams.height,
- base: startParams.height,
- });
- const { y } = snapScaleAxes({
- proposedScaleX: 1,
- proposedScaleY,
- position: geometry.position,
- baseWidth,
- baseHeight,
- rotation: proposedParams.rotation,
- canvasSize: localCanvasSize,
- snapThreshold,
- preferredEdges,
- });
-
- return {
- params: {
- ...proposedParams,
- height: Math.max(
- MIN_MASK_DIMENSION,
- startParams.height * y.snappedScale,
- ),
- },
- activeLines: toGlobalMaskSnapLines({
- lines: y.activeLines,
- bounds,
- canvasSize,
- }),
- };
- }
-
- if (handleId === "scale") {
- const baseScale = Math.max(startParams.scale, MIN_MASK_DIMENSION);
- const proposedScale = getClampedRatio({
- next: proposedParams.scale,
- base: startParams.scale,
- });
- const { snappedScale, activeLines } = snapScale({
- proposedScale,
- position: geometry.position,
- baseWidth: baseWidth * baseScale,
- baseHeight: baseHeight * baseScale,
- rotation: proposedParams.rotation,
- canvasSize: localCanvasSize,
- snapThreshold,
- preferredEdges,
- });
-
- return {
- params: {
- ...proposedParams,
- scale: Math.max(MIN_MASK_DIMENSION, startParams.scale * snappedScale),
- },
- activeLines: toGlobalMaskSnapLines({
- lines: activeLines,
- bounds,
- canvasSize,
- }),
- };
- }
-
- if (CORNER_SIZE_HANDLES.has(handleId)) {
- const proposedScale = getClampedRatio({
- next: proposedParams.width,
- base: startParams.width,
- });
- const { snappedScale, activeLines } = snapScale({
- proposedScale,
- position: geometry.position,
- baseWidth,
- baseHeight,
- rotation: proposedParams.rotation,
- canvasSize: localCanvasSize,
- snapThreshold,
- preferredEdges,
- });
-
- return {
- params: {
- ...proposedParams,
- width: Math.max(MIN_MASK_DIMENSION, startParams.width * snappedScale),
- height: Math.max(MIN_MASK_DIMENSION, startParams.height * snappedScale),
- },
- activeLines: toGlobalMaskSnapLines({
- lines: activeLines,
- bounds,
- canvasSize,
- }),
- };
- }
-
- return { params: proposedParams, activeLines: [] };
-}
-
-export function snapMaskInteraction({
- handleId,
- startParams,
- proposedParams,
- bounds,
- canvasSize,
- snapThreshold,
-}: {
- handleId: string;
- startParams: ParamValues;
- proposedParams: ParamValues;
- bounds: ElementBounds;
- canvasSize: { width: number; height: number };
- snapThreshold: { x: number; y: number };
-}): MaskSnapResult {
- if (handleId === "position") {
- return snapMaskPosition({
- proposedParams,
- bounds,
- canvasSize,
- snapThreshold,
- });
- }
-
- if (handleId === "rotation") {
- return snapMaskRotation({ proposedParams });
- }
-
- return snapBoxMaskSize({
- handleId,
- startParams,
- proposedParams,
- bounds,
- canvasSize,
- snapThreshold,
- });
-}
-
+import type { ElementBounds } from "@/lib/preview/element-bounds";
+import { MIN_MASK_DIMENSION } from "@/constants/mask-constants";
+import {
+ snapPosition,
+ snapRotation,
+ snapScale,
+ snapScaleAxes,
+ type ScaleEdgePreference,
+ type SnapLine,
+} from "@/lib/preview/preview-snap";
+import type { ParamValues } from "@/lib/params";
+import {
+ isRectangleMaskParams,
+ getMaskSnapGeometry,
+ setMaskLocalCenter,
+ toGlobalMaskSnapLines,
+} from "./geometry";
+
+type MaskSnapResult = {
+ params: ParamValues;
+ activeLines: SnapLine[];
+};
+
+const CORNER_SIZE_HANDLES = new Set([
+ "top-left",
+ "top-right",
+ "bottom-left",
+ "bottom-right",
+]);
+
+function getClampedRatio({
+ next,
+ base,
+}: {
+ next: number;
+ base: number;
+}): number {
+ return (
+ Math.max(next, MIN_MASK_DIMENSION) / Math.max(base, MIN_MASK_DIMENSION)
+ );
+}
+
+function getPreferredEdges({
+ handleId,
+}: {
+ handleId: string;
+}): ScaleEdgePreference | undefined {
+ const preferredEdges = {
+ left:
+ handleId === "left" ||
+ handleId === "top-left" ||
+ handleId === "bottom-left",
+ right:
+ handleId === "right" ||
+ handleId === "top-right" ||
+ handleId === "bottom-right",
+ top:
+ handleId === "top" || handleId === "top-left" || handleId === "top-right",
+ bottom:
+ handleId === "bottom" ||
+ handleId === "bottom-left" ||
+ handleId === "bottom-right",
+ } satisfies ScaleEdgePreference;
+
+ return Object.values(preferredEdges).some(Boolean)
+ ? preferredEdges
+ : undefined;
+}
+
+function snapMaskPosition({
+ proposedParams,
+ bounds,
+ canvasSize,
+ snapThreshold,
+}: {
+ proposedParams: ParamValues;
+ bounds: ElementBounds;
+ canvasSize: { width: number; height: number };
+ snapThreshold: { x: number; y: number };
+}): MaskSnapResult {
+ const geometry = getMaskSnapGeometry({
+ params: proposedParams,
+ bounds,
+ });
+ if (!geometry) {
+ return { params: proposedParams, activeLines: [] };
+ }
+
+ const { snappedPosition, activeLines } = snapPosition({
+ proposedPosition: geometry.position,
+ canvasSize: bounds,
+ elementSize: geometry.size,
+ rotation: geometry.rotation,
+ snapThreshold,
+ });
+
+ return {
+ params: {
+ ...proposedParams,
+ ...setMaskLocalCenter({
+ center: snappedPosition,
+ bounds,
+ }),
+ },
+ activeLines: toGlobalMaskSnapLines({
+ lines: activeLines,
+ bounds,
+ canvasSize,
+ }),
+ };
+}
+
+function snapMaskRotation({
+ proposedParams,
+}: {
+ proposedParams: ParamValues;
+}): MaskSnapResult {
+ if (typeof proposedParams.rotation !== "number") {
+ return { params: proposedParams, activeLines: [] };
+ }
+
+ const { snappedRotation } = snapRotation({
+ proposedRotation: proposedParams.rotation,
+ });
+
+ return {
+ params: {
+ ...proposedParams,
+ rotation: snappedRotation,
+ },
+ activeLines: [],
+ };
+}
+
+function snapBoxMaskSize({
+ handleId,
+ startParams,
+ proposedParams,
+ bounds,
+ canvasSize,
+ snapThreshold,
+}: {
+ handleId: string;
+ startParams: ParamValues;
+ proposedParams: ParamValues;
+ bounds: ElementBounds;
+ canvasSize: { width: number; height: number };
+ snapThreshold: { x: number; y: number };
+}): MaskSnapResult {
+ if (
+ !isRectangleMaskParams(startParams) ||
+ !isRectangleMaskParams(proposedParams)
+ ) {
+ return { params: proposedParams, activeLines: [] };
+ }
+
+ const geometry = getMaskSnapGeometry({
+ params: proposedParams,
+ bounds,
+ });
+ if (!geometry) {
+ return { params: proposedParams, activeLines: [] };
+ }
+
+ const localCanvasSize = bounds;
+ const baseWidth =
+ Math.max(startParams.width, MIN_MASK_DIMENSION) * bounds.width;
+ const baseHeight =
+ Math.max(startParams.height, MIN_MASK_DIMENSION) * bounds.height;
+ const preferredEdges = getPreferredEdges({ handleId });
+
+ if (handleId === "right" || handleId === "left") {
+ const proposedScaleX = getClampedRatio({
+ next: proposedParams.width,
+ base: startParams.width,
+ });
+ const { x } = snapScaleAxes({
+ proposedScaleX,
+ proposedScaleY: 1,
+ position: geometry.position,
+ baseWidth,
+ baseHeight,
+ rotation: proposedParams.rotation,
+ canvasSize: localCanvasSize,
+ snapThreshold,
+ preferredEdges,
+ });
+
+ return {
+ params: {
+ ...proposedParams,
+ width: Math.max(MIN_MASK_DIMENSION, startParams.width * x.snappedScale),
+ },
+ activeLines: toGlobalMaskSnapLines({
+ lines: x.activeLines,
+ bounds,
+ canvasSize,
+ }),
+ };
+ }
+
+ if (handleId === "top" || handleId === "bottom") {
+ const proposedScaleY = getClampedRatio({
+ next: proposedParams.height,
+ base: startParams.height,
+ });
+ const { y } = snapScaleAxes({
+ proposedScaleX: 1,
+ proposedScaleY,
+ position: geometry.position,
+ baseWidth,
+ baseHeight,
+ rotation: proposedParams.rotation,
+ canvasSize: localCanvasSize,
+ snapThreshold,
+ preferredEdges,
+ });
+
+ return {
+ params: {
+ ...proposedParams,
+ height: Math.max(
+ MIN_MASK_DIMENSION,
+ startParams.height * y.snappedScale,
+ ),
+ },
+ activeLines: toGlobalMaskSnapLines({
+ lines: y.activeLines,
+ bounds,
+ canvasSize,
+ }),
+ };
+ }
+
+ if (handleId === "scale") {
+ const baseScale = Math.max(startParams.scale, MIN_MASK_DIMENSION);
+ const proposedScale = getClampedRatio({
+ next: proposedParams.scale,
+ base: startParams.scale,
+ });
+ const { snappedScale, activeLines } = snapScale({
+ proposedScale,
+ position: geometry.position,
+ baseWidth: baseWidth * baseScale,
+ baseHeight: baseHeight * baseScale,
+ rotation: proposedParams.rotation,
+ canvasSize: localCanvasSize,
+ snapThreshold,
+ preferredEdges,
+ });
+
+ return {
+ params: {
+ ...proposedParams,
+ scale: Math.max(MIN_MASK_DIMENSION, startParams.scale * snappedScale),
+ },
+ activeLines: toGlobalMaskSnapLines({
+ lines: activeLines,
+ bounds,
+ canvasSize,
+ }),
+ };
+ }
+
+ if (CORNER_SIZE_HANDLES.has(handleId)) {
+ const proposedScale = getClampedRatio({
+ next: proposedParams.width,
+ base: startParams.width,
+ });
+ const { snappedScale, activeLines } = snapScale({
+ proposedScale,
+ position: geometry.position,
+ baseWidth,
+ baseHeight,
+ rotation: proposedParams.rotation,
+ canvasSize: localCanvasSize,
+ snapThreshold,
+ preferredEdges,
+ });
+
+ return {
+ params: {
+ ...proposedParams,
+ width: Math.max(MIN_MASK_DIMENSION, startParams.width * snappedScale),
+ height: Math.max(MIN_MASK_DIMENSION, startParams.height * snappedScale),
+ },
+ activeLines: toGlobalMaskSnapLines({
+ lines: activeLines,
+ bounds,
+ canvasSize,
+ }),
+ };
+ }
+
+ return { params: proposedParams, activeLines: [] };
+}
+
+export function snapMaskInteraction({
+ handleId,
+ startParams,
+ proposedParams,
+ bounds,
+ canvasSize,
+ snapThreshold,
+}: {
+ handleId: string;
+ startParams: ParamValues;
+ proposedParams: ParamValues;
+ bounds: ElementBounds;
+ canvasSize: { width: number; height: number };
+ snapThreshold: { x: number; y: number };
+}): MaskSnapResult {
+ if (handleId === "position") {
+ return snapMaskPosition({
+ proposedParams,
+ bounds,
+ canvasSize,
+ snapThreshold,
+ });
+ }
+
+ if (handleId === "rotation") {
+ return snapMaskRotation({ proposedParams });
+ }
+
+ return snapBoxMaskSize({
+ handleId,
+ startParams,
+ proposedParams,
+ bounds,
+ canvasSize,
+ snapThreshold,
+ });
+}
+