376 lines
8.0 KiB
TypeScript
376 lines
8.0 KiB
TypeScript
import type { ElementBounds } from "@/preview/element-bounds";
|
|
import { MIN_MASK_DIMENSION } from "@/masks/dimensions";
|
|
import {
|
|
snapPosition,
|
|
snapRotation,
|
|
snapScale,
|
|
snapScaleAxes,
|
|
type ScaleEdgePreference,
|
|
} from "@/preview/preview-snap";
|
|
import type {
|
|
MaskHandleId,
|
|
MaskSnapArgs,
|
|
MaskSnapResult,
|
|
RectangleMaskParams,
|
|
SplitMaskParams,
|
|
} from "@/masks/types";
|
|
import {
|
|
getMaskSnapGeometry,
|
|
setMaskLocalCenter,
|
|
toGlobalMaskSnapLines,
|
|
} from "./geometry";
|
|
|
|
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: MaskHandleId;
|
|
}): ScaleEdgePreference | undefined {
|
|
if (handleId.kind === "edge") {
|
|
return {
|
|
left: handleId.side === "left",
|
|
right: handleId.side === "right",
|
|
top: handleId.side === "top",
|
|
bottom: handleId.side === "bottom",
|
|
};
|
|
}
|
|
|
|
if (handleId.kind === "corner") {
|
|
return {
|
|
left: handleId.corner.x === "left",
|
|
right: handleId.corner.x === "right",
|
|
top: handleId.corner.y === "top",
|
|
bottom: handleId.corner.y === "bottom",
|
|
};
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function snapMaskPosition({
|
|
proposedParams,
|
|
bounds,
|
|
canvasSize,
|
|
snapThreshold,
|
|
}: {
|
|
proposedParams: RectangleMaskParams;
|
|
bounds: ElementBounds;
|
|
canvasSize: { width: number; height: number };
|
|
snapThreshold: { x: number; y: number };
|
|
}): MaskSnapResult<RectangleMaskParams>;
|
|
function snapMaskPosition({
|
|
proposedParams,
|
|
bounds,
|
|
canvasSize,
|
|
snapThreshold,
|
|
}: {
|
|
proposedParams: SplitMaskParams;
|
|
bounds: ElementBounds;
|
|
canvasSize: { width: number; height: number };
|
|
snapThreshold: { x: number; y: number };
|
|
}): MaskSnapResult<SplitMaskParams>;
|
|
function snapMaskPosition({
|
|
proposedParams,
|
|
bounds,
|
|
canvasSize,
|
|
snapThreshold,
|
|
}: {
|
|
proposedParams: RectangleMaskParams | SplitMaskParams;
|
|
bounds: ElementBounds;
|
|
canvasSize: { width: number; height: number };
|
|
snapThreshold: { x: number; y: number };
|
|
}): MaskSnapResult<RectangleMaskParams | SplitMaskParams> {
|
|
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: RectangleMaskParams;
|
|
}): MaskSnapResult<RectangleMaskParams>;
|
|
function snapMaskRotation({
|
|
proposedParams,
|
|
}: {
|
|
proposedParams: SplitMaskParams;
|
|
}): MaskSnapResult<SplitMaskParams>;
|
|
function snapMaskRotation({
|
|
proposedParams,
|
|
}: {
|
|
proposedParams: RectangleMaskParams | SplitMaskParams;
|
|
}): MaskSnapResult<RectangleMaskParams | SplitMaskParams> {
|
|
const { snappedRotation } = snapRotation({
|
|
proposedRotation: proposedParams.rotation,
|
|
});
|
|
|
|
return {
|
|
params: {
|
|
...proposedParams,
|
|
rotation: snappedRotation,
|
|
},
|
|
activeLines: [],
|
|
};
|
|
}
|
|
|
|
function snapBoxMaskSize({
|
|
handleId,
|
|
startParams,
|
|
proposedParams,
|
|
bounds,
|
|
canvasSize,
|
|
snapThreshold,
|
|
}: {
|
|
handleId: MaskHandleId;
|
|
startParams: RectangleMaskParams;
|
|
proposedParams: RectangleMaskParams;
|
|
bounds: ElementBounds;
|
|
canvasSize: { width: number; height: number };
|
|
snapThreshold: { x: number; y: number };
|
|
}): MaskSnapResult<RectangleMaskParams> {
|
|
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.kind === "edge" &&
|
|
(handleId.side === "right" || handleId.side === "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.kind === "edge" &&
|
|
(handleId.side === "top" || handleId.side === "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.kind === "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 (handleId.kind === "corner") {
|
|
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 snapBoxMaskInteraction({
|
|
handleId,
|
|
startParams,
|
|
proposedParams,
|
|
bounds,
|
|
canvasSize,
|
|
snapThreshold,
|
|
}: {
|
|
handleId: MaskHandleId;
|
|
startParams: RectangleMaskParams;
|
|
proposedParams: RectangleMaskParams;
|
|
bounds: ElementBounds;
|
|
canvasSize: { width: number; height: number };
|
|
snapThreshold: { x: number; y: number };
|
|
}): MaskSnapResult<RectangleMaskParams> {
|
|
if (handleId.kind === "position") {
|
|
return snapMaskPosition({
|
|
proposedParams,
|
|
bounds,
|
|
canvasSize,
|
|
snapThreshold,
|
|
});
|
|
}
|
|
|
|
if (handleId.kind === "rotation") {
|
|
return snapMaskRotation({ proposedParams });
|
|
}
|
|
|
|
return snapBoxMaskSize({
|
|
handleId,
|
|
startParams,
|
|
proposedParams,
|
|
bounds,
|
|
canvasSize,
|
|
snapThreshold,
|
|
});
|
|
}
|
|
|
|
export function snapSplitMaskInteraction({
|
|
handleId,
|
|
proposedParams,
|
|
bounds,
|
|
canvasSize,
|
|
snapThreshold,
|
|
}: MaskSnapArgs<SplitMaskParams>): MaskSnapResult<SplitMaskParams> {
|
|
if (handleId.kind === "position") {
|
|
return snapMaskPosition({
|
|
proposedParams,
|
|
bounds,
|
|
canvasSize,
|
|
snapThreshold,
|
|
});
|
|
}
|
|
|
|
if (handleId.kind === "rotation") {
|
|
return snapMaskRotation({ proposedParams });
|
|
}
|
|
|
|
return { params: proposedParams, activeLines: [] };
|
|
}
|