summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/masks/handle-positions.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
commit3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch)
tree57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/lib/masks/handle-positions.ts
parent729d10592f4e802f91c555c309ff3ba4b119f843 (diff)
downloadopencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/lib/masks/handle-positions.ts')
-rw-r--r--apps/web/src/lib/masks/handle-positions.ts497
1 files changed, 0 insertions, 497 deletions
diff --git a/apps/web/src/lib/masks/handle-positions.ts b/apps/web/src/lib/masks/handle-positions.ts
deleted file mode 100644
index d32b8424..00000000
--- a/apps/web/src/lib/masks/handle-positions.ts
+++ /dev/null
@@ -1,497 +0,0 @@
-import { FEATHER_HANDLE_SCALE } from "@/lib/masks/feather";
-import type { ElementBounds } from "@/lib/preview/element-bounds";
-import type {
- MaskFeatures,
- MaskHandlePosition,
- MaskLineOverlay,
- MaskOverlay,
- MaskRectOverlay,
- RectangleMaskParams,
- MaskShapeOverlay,
-} from "@/lib/masks/types";
-
-const LINE_HANDLE_OFFSET_SCREEN_PX = 20;
-const BOX_HANDLE_OFFSET_SCREEN_PX = 20;
-const LINE_EXTENT_MULTIPLIER = 50;
-
-const CURSOR = {
- rotate: "crosshair",
- resizeDiagonal: "nwse-resize",
- resizeHorizontal: "ew-resize",
- resizeVertical: "ns-resize",
-} as const;
-
-/**
- * The renderer defines the split line as:
- * - normal direction: (cos(rotation), sin(rotation))
- * - line direction (parallel to cut): (-sin(rotation), cos(rotation))
- * - reference point: (centerX * width, centerY * height) from element centre
- *
- * So rotation=0 → normal points right → line runs vertically.
- */
-export function getLineMaskLinePoints({
- centerX,
- centerY,
- rotation,
- bounds,
-}: {
- centerX: number;
- centerY: number;
- rotation: number;
- bounds: ElementBounds;
-}): { start: { x: number; y: number }; end: { x: number; y: number } } {
- const angleRad = (rotation * Math.PI) / 180;
- const normalX = Math.cos(angleRad);
- const normalY = Math.sin(angleRad);
- const lineDirX = -normalY;
- const lineDirY = normalX;
-
- const cx = bounds.cx + centerX * bounds.width;
- const cy = bounds.cy + centerY * bounds.height;
-
- const extent = Math.max(bounds.width, bounds.height) * LINE_EXTENT_MULTIPLIER;
-
- return {
- start: {
- x: cx - lineDirX * extent,
- y: cy - lineDirY * extent,
- },
- end: {
- x: cx + lineDirX * extent,
- y: cy + lineDirY * extent,
- },
- };
-}
-
-export function getLineMaskOverlay({
- centerX,
- centerY,
- rotation,
- bounds,
- handleId = "position",
- cursor = "move",
-}: {
- centerX: number;
- centerY: number;
- rotation: number;
- bounds: ElementBounds;
- handleId?: string;
- cursor?: string;
-}): MaskLineOverlay {
- const { start, end } = getLineMaskLinePoints({
- centerX,
- centerY,
- rotation,
- bounds,
- });
-
- return {
- id: "line",
- type: "line",
- start,
- end,
- handleId,
- cursor,
- };
-}
-
-export function getLineMaskHandlePositions({
- centerX,
- centerY,
- rotation,
- feather,
- bounds,
- displayScale,
-}: {
- centerX: number;
- centerY: number;
- rotation: number;
- feather: number;
- bounds: ElementBounds;
- displayScale: number;
-}): MaskHandlePosition[] {
- const angleRad = (rotation * Math.PI) / 180;
- const normalX = Math.cos(angleRad);
- const normalY = Math.sin(angleRad);
-
- const cx = bounds.cx + centerX * bounds.width;
- const cy = bounds.cy + centerY * bounds.height;
-
- const iconOffsetCanvas = LINE_HANDLE_OFFSET_SCREEN_PX / displayScale;
- const featherOffset = iconOffsetCanvas + feather * FEATHER_HANDLE_SCALE;
-
- return [
- {
- id: "rotation",
- x: cx + normalX * iconOffsetCanvas,
- y: cy + normalY * iconOffsetCanvas,
- cursor: CURSOR.rotate,
- kind: "icon",
- icon: "rotate",
- },
- {
- id: "feather",
- x: cx - normalX * featherOffset,
- y: cy - normalY * featherOffset,
- cursor: CURSOR.resizeHorizontal,
- kind: "icon",
- icon: "feather",
- },
- ];
-}
-
-function rotatePoint({
- localX,
- localY,
- cx,
- cy,
- angleRad,
-}: {
- localX: number;
- localY: number;
- cx: number;
- cy: number;
- angleRad: number;
-}): { x: number; y: number } {
- const cos = Math.cos(angleRad);
- const sin = Math.sin(angleRad);
- return {
- x: cx + localX * cos - localY * sin,
- y: cy + localX * sin + localY * cos,
- };
-}
-
-export function getBoxMaskHandlePositions({
- centerX,
- centerY,
- width,
- height,
- rotation,
- feather,
- sizeMode,
- showScaleHandle = true,
- bounds,
- displayScale,
-}: {
- centerX: number;
- centerY: number;
- width: number;
- height: number;
- rotation: number;
- feather: number;
- sizeMode: MaskFeatures["sizeMode"];
- showScaleHandle?: boolean;
- bounds: ElementBounds;
- displayScale: number;
-}): MaskHandlePosition[] {
- const cx = bounds.cx + centerX * bounds.width;
- const cy = bounds.cy + centerY * bounds.height;
- const angleRad = (rotation * Math.PI) / 180;
- const halfWidth = (width * bounds.width) / 2;
- const halfHeight = (height * bounds.height) / 2;
-
- const handles: MaskHandlePosition[] = [];
- const handleOffsetCanvas = BOX_HANDLE_OFFSET_SCREEN_PX / displayScale;
-
- const rotHandle = rotatePoint({
- localX: 0,
- localY: -halfHeight - handleOffsetCanvas,
- cx,
- cy,
- angleRad,
- });
- handles.push({
- id: "rotation",
- x: rotHandle.x,
- y: rotHandle.y,
- cursor: CURSOR.rotate,
- kind: "icon",
- icon: "rotate",
- });
-
- const featherHandle = rotatePoint({
- localX: 0,
- localY: halfHeight + handleOffsetCanvas + feather * FEATHER_HANDLE_SCALE,
- cx,
- cy,
- angleRad,
- });
- handles.push({
- id: "feather",
- x: featherHandle.x,
- y: featherHandle.y,
- cursor: CURSOR.resizeVertical,
- kind: "icon",
- icon: "feather",
- });
-
- if (sizeMode === "width-height") {
- const corners = [
- { localX: -halfWidth, localY: -halfHeight, id: "top-left" },
- { localX: halfWidth, localY: -halfHeight, id: "top-right" },
- { localX: halfWidth, localY: halfHeight, id: "bottom-right" },
- { localX: -halfWidth, localY: halfHeight, id: "bottom-left" },
- ];
- for (const { localX, localY, id } of corners) {
- const point = rotatePoint({ localX, localY, cx, cy, angleRad });
- handles.push({
- id,
- x: point.x,
- y: point.y,
- cursor: CURSOR.resizeDiagonal,
- kind: "corner",
- });
- }
- const right = rotatePoint({
- localX: halfWidth,
- localY: 0,
- cx,
- cy,
- angleRad,
- });
- const left = rotatePoint({
- localX: -halfWidth,
- localY: 0,
- cx,
- cy,
- angleRad,
- });
- const bottom = rotatePoint({
- localX: 0,
- localY: halfHeight,
- cx,
- cy,
- angleRad,
- });
- handles.push({
- id: "left",
- x: left.x,
- y: left.y,
- cursor: CURSOR.resizeHorizontal,
- kind: "edge",
- edgeAxis: "horizontal",
- rotation,
- });
- handles.push({
- id: "right",
- x: right.x,
- y: right.y,
- cursor: CURSOR.resizeHorizontal,
- kind: "edge",
- edgeAxis: "horizontal",
- rotation,
- });
- handles.push({
- id: "bottom",
- x: bottom.x,
- y: bottom.y,
- cursor: CURSOR.resizeVertical,
- kind: "edge",
- edgeAxis: "vertical",
- rotation,
- });
- } else if (sizeMode === "height-only") {
- const top = rotatePoint({
- localX: 0,
- localY: -halfHeight,
- cx,
- cy,
- angleRad,
- });
- const bottom = rotatePoint({
- localX: 0,
- localY: halfHeight,
- cx,
- cy,
- angleRad,
- });
- handles.push({
- id: "top",
- x: top.x,
- y: top.y,
- cursor: CURSOR.resizeVertical,
- kind: "edge",
- edgeAxis: "vertical",
- rotation,
- });
- handles.push({
- id: "bottom",
- x: bottom.x,
- y: bottom.y,
- cursor: CURSOR.resizeVertical,
- kind: "edge",
- edgeAxis: "vertical",
- rotation,
- });
- } else if (sizeMode === "width-only") {
- const left = rotatePoint({
- localX: -halfWidth,
- localY: 0,
- cx,
- cy,
- angleRad,
- });
- const right = rotatePoint({
- localX: halfWidth,
- localY: 0,
- cx,
- cy,
- angleRad,
- });
- handles.push({
- id: "left",
- x: left.x,
- y: left.y,
- cursor: CURSOR.resizeHorizontal,
- kind: "edge",
- edgeAxis: "horizontal",
- rotation,
- });
- handles.push({
- id: "right",
- x: right.x,
- y: right.y,
- cursor: CURSOR.resizeHorizontal,
- kind: "edge",
- edgeAxis: "horizontal",
- rotation,
- });
- } else if (sizeMode === "uniform" && showScaleHandle) {
- const point = rotatePoint({
- localX: halfWidth,
- localY: halfHeight,
- cx,
- cy,
- angleRad,
- });
- handles.push({
- id: "scale",
- x: point.x,
- y: point.y,
- cursor: CURSOR.resizeDiagonal,
- kind: "corner",
- });
- }
-
- return handles;
-}
-
-export function getBoxMaskRectOverlay({
- centerX,
- centerY,
- width,
- height,
- rotation,
- bounds,
- handleId = "position",
- cursor = "move",
- dashed = false,
-}: {
- centerX: number;
- centerY: number;
- width: number;
- height: number;
- rotation: number;
- bounds: ElementBounds;
- handleId?: string;
- cursor?: string;
- dashed?: boolean;
-}): MaskRectOverlay {
- return {
- id: "bounding-box",
- type: "rect",
- center: {
- x: bounds.cx + centerX * bounds.width,
- y: bounds.cy + centerY * bounds.height,
- },
- width: width * bounds.width,
- height: height * bounds.height,
- rotation,
- handleId,
- cursor,
- dashed,
- };
-}
-
-export function getBoxMaskShapeOverlay({
- centerX,
- centerY,
- width,
- height,
- rotation,
- bounds,
- pathData,
- handleId = "position",
- cursor = "move",
-}: {
- centerX: number;
- centerY: number;
- width: number;
- height: number;
- rotation: number;
- bounds: ElementBounds;
- pathData: string;
- handleId?: string;
- cursor?: string;
-}): MaskShapeOverlay {
- return {
- id: "shape-outline",
- type: "shape",
- center: {
- x: bounds.cx + centerX * bounds.width,
- y: bounds.cy + centerY * bounds.height,
- },
- width: width * bounds.width,
- height: height * bounds.height,
- rotation,
- pathData,
- handleId,
- cursor,
- };
-}
-
-export function getBoxMaskOverlays({
- params,
- bounds,
- pathData,
- showBoundingBox = true,
-}: {
- params: Pick<
- RectangleMaskParams,
- "centerX" | "centerY" | "width" | "height" | "rotation"
- >;
- bounds: ElementBounds;
- pathData?: string;
- showBoundingBox?: boolean;
-}): MaskOverlay[] {
- const overlays: MaskOverlay[] = [];
- if (showBoundingBox) {
- overlays.push(
- getBoxMaskRectOverlay({
- centerX: params.centerX,
- centerY: params.centerY,
- width: params.width,
- height: params.height,
- rotation: params.rotation,
- bounds,
- dashed: Boolean(pathData),
- }),
- );
- }
-
- if (pathData) {
- overlays.push(
- getBoxMaskShapeOverlay({
- centerX: params.centerX,
- centerY: params.centerY,
- width: params.width,
- height: params.height,
- rotation: params.rotation,
- bounds,
- pathData,
- }),
- );
- }
-
- return overlays;
-}