parent
ae1292a14b
commit
97f8d1968f
|
|
@ -150,10 +150,10 @@ function buildCustomMaskParams(
|
||||||
function sortSegment(
|
function sortSegment(
|
||||||
segment: [{ x: number; y: number }, { x: number; y: number }],
|
segment: [{ x: number; y: number }, { x: number; y: number }],
|
||||||
): [{ 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 [
|
const [first, second] = segment;
|
||||||
{ x: number; y: number },
|
return first.x < second.x || (first.x === second.x && first.y <= second.y)
|
||||||
{ x: number; y: number },
|
? [first, second]
|
||||||
];
|
: [second, first];
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("mask geometry", () => {
|
describe("mask geometry", () => {
|
||||||
|
|
@ -231,7 +231,7 @@ describe("mask geometry", () => {
|
||||||
describe("mask snapping", () => {
|
describe("mask snapping", () => {
|
||||||
test("snaps split mask movement using the shared position pipeline", () => {
|
test("snaps split mask movement using the shared position pipeline", () => {
|
||||||
const result = snapSplitMaskInteraction({
|
const result = snapSplitMaskInteraction({
|
||||||
handleId: "position",
|
handleId: { kind: "position" },
|
||||||
startParams: buildSplitParams({
|
startParams: buildSplitParams({
|
||||||
centerX: 0.03,
|
centerX: 0.03,
|
||||||
centerY: -0.04,
|
centerY: -0.04,
|
||||||
|
|
@ -255,7 +255,7 @@ describe("mask snapping", () => {
|
||||||
|
|
||||||
test("snaps box mask movement against element center and edges", () => {
|
test("snaps box mask movement against element center and edges", () => {
|
||||||
const result = snapBoxMaskInteraction({
|
const result = snapBoxMaskInteraction({
|
||||||
handleId: "position",
|
handleId: { kind: "position" },
|
||||||
startParams: buildRectangleParams(),
|
startParams: buildRectangleParams(),
|
||||||
proposedParams: buildRectangleParams({
|
proposedParams: buildRectangleParams({
|
||||||
centerX: 0.29,
|
centerX: 0.29,
|
||||||
|
|
@ -276,7 +276,7 @@ describe("mask snapping", () => {
|
||||||
|
|
||||||
test("snaps mask rotation through the shared rotation path", () => {
|
test("snaps mask rotation through the shared rotation path", () => {
|
||||||
const result = snapBoxMaskInteraction({
|
const result = snapBoxMaskInteraction({
|
||||||
handleId: "rotation",
|
handleId: { kind: "rotation" },
|
||||||
startParams: buildRectangleParams(),
|
startParams: buildRectangleParams(),
|
||||||
proposedParams: buildRectangleParams({
|
proposedParams: buildRectangleParams({
|
||||||
rotation: 88,
|
rotation: 88,
|
||||||
|
|
@ -292,7 +292,7 @@ describe("mask snapping", () => {
|
||||||
|
|
||||||
test("snaps edge resize for box masks", () => {
|
test("snaps edge resize for box masks", () => {
|
||||||
const result = snapBoxMaskInteraction({
|
const result = snapBoxMaskInteraction({
|
||||||
handleId: "right",
|
handleId: { kind: "edge", side: "right" },
|
||||||
startParams: buildRectangleParams(),
|
startParams: buildRectangleParams(),
|
||||||
proposedParams: buildRectangleParams({
|
proposedParams: buildRectangleParams({
|
||||||
width: 0.98,
|
width: 0.98,
|
||||||
|
|
@ -308,7 +308,7 @@ describe("mask snapping", () => {
|
||||||
|
|
||||||
test("snaps corner resize for box masks", () => {
|
test("snaps corner resize for box masks", () => {
|
||||||
const result = snapBoxMaskInteraction({
|
const result = snapBoxMaskInteraction({
|
||||||
handleId: "bottom-right",
|
handleId: { kind: "corner", corner: { x: "right", y: "bottom" } },
|
||||||
startParams: buildRectangleParams(),
|
startParams: buildRectangleParams(),
|
||||||
proposedParams: buildRectangleParams({
|
proposedParams: buildRectangleParams({
|
||||||
width: 0.99,
|
width: 0.99,
|
||||||
|
|
@ -330,7 +330,7 @@ describe("mask snapping", () => {
|
||||||
centerY: -0.04,
|
centerY: -0.04,
|
||||||
});
|
});
|
||||||
const result = textMaskDefinition.interaction.snap?.({
|
const result = textMaskDefinition.interaction.snap?.({
|
||||||
handleId: "position",
|
handleId: { kind: "position" },
|
||||||
startParams: params,
|
startParams: params,
|
||||||
proposedParams: params,
|
proposedParams: params,
|
||||||
bounds,
|
bounds,
|
||||||
|
|
@ -352,7 +352,7 @@ describe("mask snapping", () => {
|
||||||
centerY: -0.04,
|
centerY: -0.04,
|
||||||
});
|
});
|
||||||
const result = customMaskDefinition.interaction.snap?.({
|
const result = customMaskDefinition.interaction.snap?.({
|
||||||
handleId: "position",
|
handleId: { kind: "position" },
|
||||||
startParams: params,
|
startParams: params,
|
||||||
proposedParams: params,
|
proposedParams: params,
|
||||||
bounds,
|
bounds,
|
||||||
|
|
|
||||||
|
|
@ -10,56 +10,29 @@ export interface CustomMaskPathPoint {
|
||||||
outY: number;
|
outY: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CustomMaskHandlePart = "anchor" | "in" | "out";
|
|
||||||
|
|
||||||
function isCustomMaskPathPoint(value: unknown): value is CustomMaskPathPoint {
|
function isCustomMaskPathPoint(value: unknown): value is CustomMaskPathPoint {
|
||||||
if (!value || typeof value !== "object") {
|
if (!value || typeof value !== "object") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const candidate = value as Record<string, unknown>;
|
|
||||||
return (
|
return (
|
||||||
typeof candidate.id === "string" &&
|
"id" in value &&
|
||||||
typeof candidate.x === "number" &&
|
typeof value.id === "string" &&
|
||||||
typeof candidate.y === "number" &&
|
"x" in value &&
|
||||||
typeof candidate.inX === "number" &&
|
typeof value.x === "number" &&
|
||||||
typeof candidate.inY === "number" &&
|
"y" in value &&
|
||||||
typeof candidate.outX === "number" &&
|
typeof value.y === "number" &&
|
||||||
typeof candidate.outY === "number"
|
"inX" in value &&
|
||||||
|
typeof value.inX === "number" &&
|
||||||
|
"inY" in value &&
|
||||||
|
typeof value.inY === "number" &&
|
||||||
|
"outX" in value &&
|
||||||
|
typeof value.outX === "number" &&
|
||||||
|
"outY" in value &&
|
||||||
|
typeof value.outY === "number"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseCustomMaskHandleId({
|
|
||||||
handleId,
|
|
||||||
}: {
|
|
||||||
handleId: string;
|
|
||||||
}): { pointId: string; part: CustomMaskHandlePart } | null {
|
|
||||||
const match = /^point:(.+):(anchor|in|out)$/.exec(handleId);
|
|
||||||
if (!match) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
pointId: match[1],
|
|
||||||
part: match[2] as CustomMaskHandlePart,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseCustomMaskSegmentHandleId({
|
|
||||||
handleId,
|
|
||||||
}: {
|
|
||||||
handleId: string;
|
|
||||||
}): { segmentIndex: number } | null {
|
|
||||||
const match = /^segment:(\d+)$/.exec(handleId);
|
|
||||||
if (!match) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
segmentIndex: Number.parseInt(match[1], 10),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseCustomMaskPath({
|
export function parseCustomMaskPath({
|
||||||
path,
|
path,
|
||||||
}: {
|
}: {
|
||||||
|
|
|
||||||
|
|
@ -210,20 +210,20 @@ export function computeBoxMaskParamUpdate({
|
||||||
deltaY,
|
deltaY,
|
||||||
bounds,
|
bounds,
|
||||||
}: MaskParamUpdateArgs<RectangleMaskParams>): Partial<RectangleMaskParams> {
|
}: MaskParamUpdateArgs<RectangleMaskParams>): Partial<RectangleMaskParams> {
|
||||||
if (handleId === "position") {
|
if (handleId.kind === "position") {
|
||||||
return {
|
return {
|
||||||
centerX: startParams.centerX + deltaX / bounds.width,
|
centerX: startParams.centerX + deltaX / bounds.width,
|
||||||
centerY: startParams.centerY + deltaY / bounds.height,
|
centerY: startParams.centerY + deltaY / bounds.height,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleId === "rotation") {
|
if (handleId.kind === "rotation") {
|
||||||
const currentAngle = (Math.atan2(deltaY, deltaX) * 180) / Math.PI;
|
const currentAngle = (Math.atan2(deltaY, deltaX) * 180) / Math.PI;
|
||||||
const newRotation = (startParams.rotation + currentAngle) % 360;
|
const newRotation = (startParams.rotation + currentAngle) % 360;
|
||||||
return { rotation: newRotation < 0 ? newRotation + 360 : newRotation };
|
return { rotation: newRotation < 0 ? newRotation + 360 : newRotation };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleId === "feather") {
|
if (handleId.kind === "feather") {
|
||||||
const angleRad = (startParams.rotation * Math.PI) / 180;
|
const angleRad = (startParams.rotation * Math.PI) / 180;
|
||||||
return computeFeatherUpdate({
|
return computeFeatherUpdate({
|
||||||
startFeather: startParams.feather,
|
startFeather: startParams.feather,
|
||||||
|
|
@ -237,8 +237,11 @@ export function computeBoxMaskParamUpdate({
|
||||||
const halfWidth = startParams.width * bounds.width;
|
const halfWidth = startParams.width * bounds.width;
|
||||||
const halfHeight = startParams.height * bounds.height;
|
const halfHeight = startParams.height * bounds.height;
|
||||||
|
|
||||||
if (handleId === "right" || handleId === "left") {
|
if (
|
||||||
const sign = handleId === "right" ? 1 : -1;
|
handleId.kind === "edge" &&
|
||||||
|
(handleId.side === "right" || handleId.side === "left")
|
||||||
|
) {
|
||||||
|
const sign = handleId.side === "right" ? 1 : -1;
|
||||||
return {
|
return {
|
||||||
width: Math.max(
|
width: Math.max(
|
||||||
MIN_MASK_DIMENSION,
|
MIN_MASK_DIMENSION,
|
||||||
|
|
@ -247,8 +250,11 @@ export function computeBoxMaskParamUpdate({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleId === "bottom" || handleId === "top") {
|
if (
|
||||||
const sign = handleId === "bottom" ? 1 : -1;
|
handleId.kind === "edge" &&
|
||||||
|
(handleId.side === "bottom" || handleId.side === "top")
|
||||||
|
) {
|
||||||
|
const sign = handleId.side === "bottom" ? 1 : -1;
|
||||||
return {
|
return {
|
||||||
height: Math.max(
|
height: Math.max(
|
||||||
MIN_MASK_DIMENSION,
|
MIN_MASK_DIMENSION,
|
||||||
|
|
@ -257,14 +263,9 @@ export function computeBoxMaskParamUpdate({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (handleId.kind === "corner") {
|
||||||
handleId === "top-left" ||
|
const signX = handleId.corner.x === "right" ? 1 : -1;
|
||||||
handleId === "top-right" ||
|
const signY = handleId.corner.y === "bottom" ? 1 : -1;
|
||||||
handleId === "bottom-left" ||
|
|
||||||
handleId === "bottom-right"
|
|
||||||
) {
|
|
||||||
const signX = handleId.includes("right") ? 1 : -1;
|
|
||||||
const signY = handleId.includes("bottom") ? 1 : -1;
|
|
||||||
const distance = Math.sqrt(
|
const distance = Math.sqrt(
|
||||||
(signX * deltaX + halfWidth) ** 2 + (signY * deltaY + halfHeight) ** 2,
|
(signX * deltaX + halfWidth) ** 2 + (signY * deltaY + halfHeight) ** 2,
|
||||||
);
|
);
|
||||||
|
|
@ -276,7 +277,7 @@ export function computeBoxMaskParamUpdate({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleId === "scale") {
|
if (handleId.kind === "scale") {
|
||||||
const distance = Math.sqrt(deltaX ** 2 + deltaY ** 2);
|
const distance = Math.sqrt(deltaX ** 2 + deltaY ** 2);
|
||||||
const originalDistance = Math.sqrt(halfWidth ** 2 + halfHeight ** 2);
|
const originalDistance = Math.sqrt(halfWidth ** 2 + halfHeight ** 2);
|
||||||
const scale = originalDistance > 0 ? 1 + distance / originalDistance : 1;
|
const scale = originalDistance > 0 ? 1 + distance / originalDistance : 1;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import {
|
||||||
getCustomMaskLocalBounds,
|
getCustomMaskLocalBounds,
|
||||||
getCustomMaskSegmentCount,
|
getCustomMaskSegmentCount,
|
||||||
insertPointIntoCustomMaskSegment,
|
insertPointIntoCustomMaskSegment,
|
||||||
parseCustomMaskHandleId,
|
|
||||||
recenterCustomMaskPath,
|
recenterCustomMaskPath,
|
||||||
type CustomMaskPathPoint,
|
type CustomMaskPathPoint,
|
||||||
} from "@/masks/custom-path";
|
} from "@/masks/custom-path";
|
||||||
|
|
@ -133,12 +132,12 @@ function getCustomMaskDisplayHandles({
|
||||||
scale: params.scale,
|
scale: params.scale,
|
||||||
bounds,
|
bounds,
|
||||||
closed: true,
|
closed: true,
|
||||||
}).map((segment) => ({
|
}).map((segment): MaskOverlay => ({
|
||||||
id: `segment:${segment.index}`,
|
id: `segment:${segment.index}`,
|
||||||
type: "canvas-path" as const,
|
type: "canvas-path" as const,
|
||||||
pathData: segment.pathData,
|
pathData: segment.pathData,
|
||||||
coordinateSpace: "canvas" as const,
|
coordinateSpace: "canvas" as const,
|
||||||
handleId: `segment:${segment.index}`,
|
handleId: { kind: "segment", index: segment.index },
|
||||||
cursor: PEN_CURSOR,
|
cursor: PEN_CURSOR,
|
||||||
strokeOpacity: 0,
|
strokeOpacity: 0,
|
||||||
strokeWidth: segmentStrokeWidth,
|
strokeWidth: segmentStrokeWidth,
|
||||||
|
|
@ -166,7 +165,7 @@ function getCustomMaskDisplayHandles({
|
||||||
|
|
||||||
geometry.anchors.forEach((point) => {
|
geometry.anchors.forEach((point) => {
|
||||||
handles.push({
|
handles.push({
|
||||||
id: `point:${point.id}:anchor`,
|
id: { kind: "anchor", pointId: point.id },
|
||||||
x: point.anchor.x,
|
x: point.anchor.x,
|
||||||
y: point.anchor.y,
|
y: point.anchor.y,
|
||||||
cursor: params.closed ? "move" : "pointer",
|
cursor: params.closed ? "move" : "pointer",
|
||||||
|
|
@ -201,7 +200,7 @@ function computeCustomMaskParamUpdate({
|
||||||
startCanvasY,
|
startCanvasY,
|
||||||
bounds,
|
bounds,
|
||||||
}: MaskParamUpdateArgs<CustomMaskParams>): Partial<CustomMaskParams> {
|
}: MaskParamUpdateArgs<CustomMaskParams>): Partial<CustomMaskParams> {
|
||||||
if (handleId === "position") {
|
if (handleId.kind === "position") {
|
||||||
return {
|
return {
|
||||||
centerX: startParams.centerX + deltaX / bounds.width,
|
centerX: startParams.centerX + deltaX / bounds.width,
|
||||||
centerY: startParams.centerY + deltaY / bounds.height,
|
centerY: startParams.centerY + deltaY / bounds.height,
|
||||||
|
|
@ -211,7 +210,7 @@ function computeCustomMaskParamUpdate({
|
||||||
const pivotX = bounds.cx + startParams.centerX * bounds.width;
|
const pivotX = bounds.cx + startParams.centerX * bounds.width;
|
||||||
const pivotY = bounds.cy + startParams.centerY * bounds.height;
|
const pivotY = bounds.cy + startParams.centerY * bounds.height;
|
||||||
|
|
||||||
if (handleId === "rotation") {
|
if (handleId.kind === "rotation") {
|
||||||
const startAngle =
|
const startAngle =
|
||||||
(Math.atan2(startCanvasY - pivotY, startCanvasX - pivotX) * 180) /
|
(Math.atan2(startCanvasY - pivotY, startCanvasX - pivotX) * 180) /
|
||||||
Math.PI;
|
Math.PI;
|
||||||
|
|
@ -232,7 +231,7 @@ function computeCustomMaskParamUpdate({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleId === "feather") {
|
if (handleId.kind === "feather") {
|
||||||
const angleRad = (startParams.rotation * Math.PI) / 180;
|
const angleRad = (startParams.rotation * Math.PI) / 180;
|
||||||
return computeFeatherUpdate({
|
return computeFeatherUpdate({
|
||||||
startFeather: startParams.feather,
|
startFeather: startParams.feather,
|
||||||
|
|
@ -243,7 +242,7 @@ function computeCustomMaskParamUpdate({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleId === "scale") {
|
if (handleId.kind === "scale") {
|
||||||
const startDistance = Math.hypot(
|
const startDistance = Math.hypot(
|
||||||
startCanvasX - pivotX,
|
startCanvasX - pivotX,
|
||||||
startCanvasY - pivotY,
|
startCanvasY - pivotY,
|
||||||
|
|
@ -258,8 +257,7 @@ function computeCustomMaskParamUpdate({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedHandle = parseCustomMaskHandleId({ handleId });
|
if (handleId.kind !== "anchor" && handleId.kind !== "tangent") {
|
||||||
if (!parsedHandle) {
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,9 +278,9 @@ function computeCustomMaskParamUpdate({
|
||||||
return {
|
return {
|
||||||
path: updateCustomMaskPoint({
|
path: updateCustomMaskPoint({
|
||||||
points,
|
points,
|
||||||
pointId: parsedHandle.pointId,
|
pointId: handleId.pointId,
|
||||||
updater: (point) => {
|
updater: (point) => {
|
||||||
if (parsedHandle.part === "anchor") {
|
if (handleId.kind === "anchor") {
|
||||||
return {
|
return {
|
||||||
...point,
|
...point,
|
||||||
x: localPoint.x,
|
x: localPoint.x,
|
||||||
|
|
@ -290,7 +288,7 @@ function computeCustomMaskParamUpdate({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedHandle.part === "in") {
|
if (handleId.side === "in") {
|
||||||
return {
|
return {
|
||||||
...point,
|
...point,
|
||||||
inX: localPoint.x - point.x,
|
inX: localPoint.x - point.x,
|
||||||
|
|
@ -349,7 +347,7 @@ export const customMaskDefinition: MaskDefinition<"custom"> = {
|
||||||
y: proposedParams.centerY * bounds.height,
|
y: proposedParams.centerY * bounds.height,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (handleId === "position") {
|
if (handleId.kind === "position") {
|
||||||
const { snappedPosition, activeLines } = snapPosition({
|
const { snappedPosition, activeLines } = snapPosition({
|
||||||
proposedPosition: position,
|
proposedPosition: position,
|
||||||
canvasSize: bounds,
|
canvasSize: bounds,
|
||||||
|
|
@ -377,7 +375,7 @@ export const customMaskDefinition: MaskDefinition<"custom"> = {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleId === "rotation") {
|
if (handleId.kind === "rotation") {
|
||||||
const { snappedRotation } = snapRotation({
|
const { snappedRotation } = snapRotation({
|
||||||
proposedRotation: proposedParams.rotation,
|
proposedRotation: proposedParams.rotation,
|
||||||
});
|
});
|
||||||
|
|
@ -390,7 +388,7 @@ export const customMaskDefinition: MaskDefinition<"custom"> = {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleId === "scale") {
|
if (handleId.kind === "scale") {
|
||||||
const { snappedScale, activeLines } = snapScale({
|
const { snappedScale, activeLines } = snapScale({
|
||||||
proposedScale: proposedParams.scale,
|
proposedScale: proposedParams.scale,
|
||||||
position,
|
position,
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ function computeSplitMaskParamUpdate({
|
||||||
bounds,
|
bounds,
|
||||||
canvasSize,
|
canvasSize,
|
||||||
}: MaskParamUpdateArgs<SplitMaskParams>): Partial<SplitMaskParams> {
|
}: MaskParamUpdateArgs<SplitMaskParams>): Partial<SplitMaskParams> {
|
||||||
if (handleId === "position") {
|
if (handleId.kind === "position") {
|
||||||
const rawX = startParams.centerX + deltaX / bounds.width;
|
const rawX = startParams.centerX + deltaX / bounds.width;
|
||||||
const rawY = startParams.centerY + deltaY / bounds.height;
|
const rawY = startParams.centerY + deltaY / bounds.height;
|
||||||
|
|
||||||
|
|
@ -143,7 +143,7 @@ function computeSplitMaskParamUpdate({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleId === "feather") {
|
if (handleId.kind === "feather") {
|
||||||
const angleRad = (startParams.rotation * Math.PI) / 180;
|
const angleRad = (startParams.rotation * Math.PI) / 180;
|
||||||
return computeFeatherUpdate({
|
return computeFeatherUpdate({
|
||||||
startFeather: startParams.feather,
|
startFeather: startParams.feather,
|
||||||
|
|
@ -154,7 +154,7 @@ function computeSplitMaskParamUpdate({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleId === "rotation") {
|
if (handleId.kind === "rotation") {
|
||||||
const pivotX = bounds.cx + startParams.centerX * bounds.width;
|
const pivotX = bounds.cx + startParams.centerX * bounds.width;
|
||||||
const pivotY = bounds.cy + startParams.centerY * bounds.height;
|
const pivotY = bounds.cy + startParams.centerY * bounds.height;
|
||||||
const startAngle =
|
const startAngle =
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import type {
|
||||||
MaskParamUpdateArgs,
|
MaskParamUpdateArgs,
|
||||||
TextMask,
|
TextMask,
|
||||||
TextMaskParams,
|
TextMaskParams,
|
||||||
|
MaskHandleId,
|
||||||
} from "@/masks/types";
|
} from "@/masks/types";
|
||||||
import { DEFAULTS } from "@/timeline/defaults";
|
import { DEFAULTS } from "@/timeline/defaults";
|
||||||
import { MIN_FONT_SIZE, MAX_FONT_SIZE } from "@/text/typography";
|
import { MIN_FONT_SIZE, MAX_FONT_SIZE } from "@/text/typography";
|
||||||
|
|
@ -124,9 +125,9 @@ function measureTextMask({
|
||||||
function getScalePreferredEdges({
|
function getScalePreferredEdges({
|
||||||
handleId,
|
handleId,
|
||||||
}: {
|
}: {
|
||||||
handleId: string;
|
handleId: MaskHandleId;
|
||||||
}): ScaleEdgePreference | undefined {
|
}): ScaleEdgePreference | undefined {
|
||||||
if (handleId !== "scale") {
|
if (handleId.kind !== "scale") {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,7 +146,7 @@ function computeTextMaskParamUpdate({
|
||||||
startCanvasY,
|
startCanvasY,
|
||||||
bounds,
|
bounds,
|
||||||
}: MaskParamUpdateArgs<TextMaskParams>): Partial<TextMaskParams> {
|
}: MaskParamUpdateArgs<TextMaskParams>): Partial<TextMaskParams> {
|
||||||
if (handleId === "position") {
|
if (handleId.kind === "position") {
|
||||||
return {
|
return {
|
||||||
centerX: startParams.centerX + deltaX / bounds.width,
|
centerX: startParams.centerX + deltaX / bounds.width,
|
||||||
centerY: startParams.centerY + deltaY / bounds.height,
|
centerY: startParams.centerY + deltaY / bounds.height,
|
||||||
|
|
@ -155,7 +156,7 @@ function computeTextMaskParamUpdate({
|
||||||
const pivotX = bounds.cx + startParams.centerX * bounds.width;
|
const pivotX = bounds.cx + startParams.centerX * bounds.width;
|
||||||
const pivotY = bounds.cy + startParams.centerY * bounds.height;
|
const pivotY = bounds.cy + startParams.centerY * bounds.height;
|
||||||
|
|
||||||
if (handleId === "rotation") {
|
if (handleId.kind === "rotation") {
|
||||||
const startAngle =
|
const startAngle =
|
||||||
(Math.atan2(startCanvasY - pivotY, startCanvasX - pivotX) * 180) /
|
(Math.atan2(startCanvasY - pivotY, startCanvasX - pivotX) * 180) /
|
||||||
Math.PI;
|
Math.PI;
|
||||||
|
|
@ -176,7 +177,7 @@ function computeTextMaskParamUpdate({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleId === "feather") {
|
if (handleId.kind === "feather") {
|
||||||
const angleRad = (startParams.rotation * Math.PI) / 180;
|
const angleRad = (startParams.rotation * Math.PI) / 180;
|
||||||
return computeFeatherUpdate({
|
return computeFeatherUpdate({
|
||||||
startFeather: startParams.feather,
|
startFeather: startParams.feather,
|
||||||
|
|
@ -187,7 +188,7 @@ function computeTextMaskParamUpdate({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleId === "scale") {
|
if (handleId.kind === "scale") {
|
||||||
const startDistance = Math.hypot(
|
const startDistance = Math.hypot(
|
||||||
startCanvasX - pivotX,
|
startCanvasX - pivotX,
|
||||||
startCanvasY - pivotY,
|
startCanvasY - pivotY,
|
||||||
|
|
@ -269,7 +270,7 @@ export const textMaskDefinition: MaskDefinition<"text"> = {
|
||||||
y: proposedParams.centerY * bounds.height,
|
y: proposedParams.centerY * bounds.height,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (handleId === "position") {
|
if (handleId.kind === "position") {
|
||||||
const { snappedPosition, activeLines } = snapPosition({
|
const { snappedPosition, activeLines } = snapPosition({
|
||||||
proposedPosition: position,
|
proposedPosition: position,
|
||||||
canvasSize: bounds,
|
canvasSize: bounds,
|
||||||
|
|
@ -297,7 +298,7 @@ export const textMaskDefinition: MaskDefinition<"text"> = {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleId === "rotation") {
|
if (handleId.kind === "rotation") {
|
||||||
const { snappedRotation } = snapRotation({
|
const { snappedRotation } = snapRotation({
|
||||||
proposedRotation: proposedParams.rotation,
|
proposedRotation: proposedParams.rotation,
|
||||||
});
|
});
|
||||||
|
|
@ -310,7 +311,7 @@ export const textMaskDefinition: MaskDefinition<"text"> = {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleId === "scale") {
|
if (handleId.kind === "scale") {
|
||||||
const { snappedScale, activeLines } = snapScale({
|
const { snappedScale, activeLines } = snapScale({
|
||||||
proposedScale: proposedParams.scale,
|
proposedScale: proposedParams.scale,
|
||||||
position,
|
position,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { FEATHER_HANDLE_SCALE } from "@/masks/feather";
|
||||||
import type { ElementBounds } from "@/preview/element-bounds";
|
import type { ElementBounds } from "@/preview/element-bounds";
|
||||||
import type {
|
import type {
|
||||||
MaskFeatures,
|
MaskFeatures,
|
||||||
|
MaskHandleId,
|
||||||
MaskHandlePosition,
|
MaskHandlePosition,
|
||||||
MaskLineOverlay,
|
MaskLineOverlay,
|
||||||
MaskOverlay,
|
MaskOverlay,
|
||||||
|
|
@ -68,14 +69,14 @@ export function getLineMaskOverlay({
|
||||||
centerY,
|
centerY,
|
||||||
rotation,
|
rotation,
|
||||||
bounds,
|
bounds,
|
||||||
handleId = "position",
|
handleId = { kind: "position" },
|
||||||
cursor = "move",
|
cursor = "move",
|
||||||
}: {
|
}: {
|
||||||
centerX: number;
|
centerX: number;
|
||||||
centerY: number;
|
centerY: number;
|
||||||
rotation: number;
|
rotation: number;
|
||||||
bounds: ElementBounds;
|
bounds: ElementBounds;
|
||||||
handleId?: string;
|
handleId?: MaskHandleId;
|
||||||
cursor?: string;
|
cursor?: string;
|
||||||
}): MaskLineOverlay {
|
}): MaskLineOverlay {
|
||||||
const { start, end } = getLineMaskLinePoints({
|
const { start, end } = getLineMaskLinePoints({
|
||||||
|
|
@ -122,7 +123,7 @@ export function getLineMaskHandlePositions({
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: "rotation",
|
id: { kind: "rotation" },
|
||||||
x: cx + normalX * iconOffsetCanvas,
|
x: cx + normalX * iconOffsetCanvas,
|
||||||
y: cy + normalY * iconOffsetCanvas,
|
y: cy + normalY * iconOffsetCanvas,
|
||||||
cursor: CURSOR.rotate,
|
cursor: CURSOR.rotate,
|
||||||
|
|
@ -130,7 +131,7 @@ export function getLineMaskHandlePositions({
|
||||||
icon: "rotate",
|
icon: "rotate",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "feather",
|
id: { kind: "feather" },
|
||||||
x: cx - normalX * featherOffset,
|
x: cx - normalX * featherOffset,
|
||||||
y: cy - normalY * featherOffset,
|
y: cy - normalY * featherOffset,
|
||||||
cursor: CURSOR.resizeHorizontal,
|
cursor: CURSOR.resizeHorizontal,
|
||||||
|
|
@ -201,7 +202,7 @@ export function getBoxMaskHandlePositions({
|
||||||
angleRad,
|
angleRad,
|
||||||
});
|
});
|
||||||
handles.push({
|
handles.push({
|
||||||
id: "rotation",
|
id: { kind: "rotation" },
|
||||||
x: rotHandle.x,
|
x: rotHandle.x,
|
||||||
y: rotHandle.y,
|
y: rotHandle.y,
|
||||||
cursor: CURSOR.rotate,
|
cursor: CURSOR.rotate,
|
||||||
|
|
@ -217,7 +218,7 @@ export function getBoxMaskHandlePositions({
|
||||||
angleRad,
|
angleRad,
|
||||||
});
|
});
|
||||||
handles.push({
|
handles.push({
|
||||||
id: "feather",
|
id: { kind: "feather" },
|
||||||
x: featherHandle.x,
|
x: featherHandle.x,
|
||||||
y: featherHandle.y,
|
y: featherHandle.y,
|
||||||
cursor: CURSOR.resizeVertical,
|
cursor: CURSOR.resizeVertical,
|
||||||
|
|
@ -226,16 +227,36 @@ export function getBoxMaskHandlePositions({
|
||||||
});
|
});
|
||||||
|
|
||||||
if (sizeMode === "width-height") {
|
if (sizeMode === "width-height") {
|
||||||
const corners = [
|
const corners: {
|
||||||
{ localX: -halfWidth, localY: -halfHeight, id: "top-left" },
|
localX: number;
|
||||||
{ localX: halfWidth, localY: -halfHeight, id: "top-right" },
|
localY: number;
|
||||||
{ localX: halfWidth, localY: halfHeight, id: "bottom-right" },
|
corner: Extract<MaskHandleId, { kind: "corner" }>["corner"];
|
||||||
{ localX: -halfWidth, localY: halfHeight, id: "bottom-left" },
|
}[] = [
|
||||||
|
{
|
||||||
|
localX: -halfWidth,
|
||||||
|
localY: -halfHeight,
|
||||||
|
corner: { x: "left", y: "top" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
localX: halfWidth,
|
||||||
|
localY: -halfHeight,
|
||||||
|
corner: { x: "right", y: "top" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
localX: halfWidth,
|
||||||
|
localY: halfHeight,
|
||||||
|
corner: { x: "right", y: "bottom" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
localX: -halfWidth,
|
||||||
|
localY: halfHeight,
|
||||||
|
corner: { x: "left", y: "bottom" },
|
||||||
|
},
|
||||||
];
|
];
|
||||||
for (const { localX, localY, id } of corners) {
|
for (const { localX, localY, corner } of corners) {
|
||||||
const point = rotatePoint({ localX, localY, cx, cy, angleRad });
|
const point = rotatePoint({ localX, localY, cx, cy, angleRad });
|
||||||
handles.push({
|
handles.push({
|
||||||
id,
|
id: { kind: "corner", corner },
|
||||||
x: point.x,
|
x: point.x,
|
||||||
y: point.y,
|
y: point.y,
|
||||||
cursor: CURSOR.resizeDiagonal,
|
cursor: CURSOR.resizeDiagonal,
|
||||||
|
|
@ -264,7 +285,7 @@ export function getBoxMaskHandlePositions({
|
||||||
angleRad,
|
angleRad,
|
||||||
});
|
});
|
||||||
handles.push({
|
handles.push({
|
||||||
id: "left",
|
id: { kind: "edge", side: "left" },
|
||||||
x: left.x,
|
x: left.x,
|
||||||
y: left.y,
|
y: left.y,
|
||||||
cursor: CURSOR.resizeHorizontal,
|
cursor: CURSOR.resizeHorizontal,
|
||||||
|
|
@ -273,7 +294,7 @@ export function getBoxMaskHandlePositions({
|
||||||
rotation,
|
rotation,
|
||||||
});
|
});
|
||||||
handles.push({
|
handles.push({
|
||||||
id: "right",
|
id: { kind: "edge", side: "right" },
|
||||||
x: right.x,
|
x: right.x,
|
||||||
y: right.y,
|
y: right.y,
|
||||||
cursor: CURSOR.resizeHorizontal,
|
cursor: CURSOR.resizeHorizontal,
|
||||||
|
|
@ -282,7 +303,7 @@ export function getBoxMaskHandlePositions({
|
||||||
rotation,
|
rotation,
|
||||||
});
|
});
|
||||||
handles.push({
|
handles.push({
|
||||||
id: "bottom",
|
id: { kind: "edge", side: "bottom" },
|
||||||
x: bottom.x,
|
x: bottom.x,
|
||||||
y: bottom.y,
|
y: bottom.y,
|
||||||
cursor: CURSOR.resizeVertical,
|
cursor: CURSOR.resizeVertical,
|
||||||
|
|
@ -306,7 +327,7 @@ export function getBoxMaskHandlePositions({
|
||||||
angleRad,
|
angleRad,
|
||||||
});
|
});
|
||||||
handles.push({
|
handles.push({
|
||||||
id: "top",
|
id: { kind: "edge", side: "top" },
|
||||||
x: top.x,
|
x: top.x,
|
||||||
y: top.y,
|
y: top.y,
|
||||||
cursor: CURSOR.resizeVertical,
|
cursor: CURSOR.resizeVertical,
|
||||||
|
|
@ -315,7 +336,7 @@ export function getBoxMaskHandlePositions({
|
||||||
rotation,
|
rotation,
|
||||||
});
|
});
|
||||||
handles.push({
|
handles.push({
|
||||||
id: "bottom",
|
id: { kind: "edge", side: "bottom" },
|
||||||
x: bottom.x,
|
x: bottom.x,
|
||||||
y: bottom.y,
|
y: bottom.y,
|
||||||
cursor: CURSOR.resizeVertical,
|
cursor: CURSOR.resizeVertical,
|
||||||
|
|
@ -339,7 +360,7 @@ export function getBoxMaskHandlePositions({
|
||||||
angleRad,
|
angleRad,
|
||||||
});
|
});
|
||||||
handles.push({
|
handles.push({
|
||||||
id: "left",
|
id: { kind: "edge", side: "left" },
|
||||||
x: left.x,
|
x: left.x,
|
||||||
y: left.y,
|
y: left.y,
|
||||||
cursor: CURSOR.resizeHorizontal,
|
cursor: CURSOR.resizeHorizontal,
|
||||||
|
|
@ -348,7 +369,7 @@ export function getBoxMaskHandlePositions({
|
||||||
rotation,
|
rotation,
|
||||||
});
|
});
|
||||||
handles.push({
|
handles.push({
|
||||||
id: "right",
|
id: { kind: "edge", side: "right" },
|
||||||
x: right.x,
|
x: right.x,
|
||||||
y: right.y,
|
y: right.y,
|
||||||
cursor: CURSOR.resizeHorizontal,
|
cursor: CURSOR.resizeHorizontal,
|
||||||
|
|
@ -365,7 +386,7 @@ export function getBoxMaskHandlePositions({
|
||||||
angleRad,
|
angleRad,
|
||||||
});
|
});
|
||||||
handles.push({
|
handles.push({
|
||||||
id: "scale",
|
id: { kind: "scale" },
|
||||||
x: point.x,
|
x: point.x,
|
||||||
y: point.y,
|
y: point.y,
|
||||||
cursor: CURSOR.resizeDiagonal,
|
cursor: CURSOR.resizeDiagonal,
|
||||||
|
|
@ -383,7 +404,7 @@ export function getBoxMaskRectOverlay({
|
||||||
height,
|
height,
|
||||||
rotation,
|
rotation,
|
||||||
bounds,
|
bounds,
|
||||||
handleId = "position",
|
handleId = { kind: "position" },
|
||||||
cursor = "move",
|
cursor = "move",
|
||||||
dashed = false,
|
dashed = false,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -393,7 +414,7 @@ export function getBoxMaskRectOverlay({
|
||||||
height: number;
|
height: number;
|
||||||
rotation: number;
|
rotation: number;
|
||||||
bounds: ElementBounds;
|
bounds: ElementBounds;
|
||||||
handleId?: string;
|
handleId?: MaskHandleId;
|
||||||
cursor?: string;
|
cursor?: string;
|
||||||
dashed?: boolean;
|
dashed?: boolean;
|
||||||
}): MaskRectOverlay {
|
}): MaskRectOverlay {
|
||||||
|
|
@ -421,7 +442,7 @@ export function getBoxMaskShapeOverlay({
|
||||||
rotation,
|
rotation,
|
||||||
bounds,
|
bounds,
|
||||||
pathData,
|
pathData,
|
||||||
handleId = "position",
|
handleId = { kind: "position" },
|
||||||
cursor = "move",
|
cursor = "move",
|
||||||
}: {
|
}: {
|
||||||
centerX: number;
|
centerX: number;
|
||||||
|
|
@ -431,7 +452,7 @@ export function getBoxMaskShapeOverlay({
|
||||||
rotation: number;
|
rotation: number;
|
||||||
bounds: ElementBounds;
|
bounds: ElementBounds;
|
||||||
pathData: string;
|
pathData: string;
|
||||||
handleId?: string;
|
handleId?: MaskHandleId;
|
||||||
cursor?: string;
|
cursor?: string;
|
||||||
}): MaskShapeOverlay {
|
}): MaskShapeOverlay {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
type ScaleEdgePreference,
|
type ScaleEdgePreference,
|
||||||
} from "@/preview/preview-snap";
|
} from "@/preview/preview-snap";
|
||||||
import type {
|
import type {
|
||||||
|
MaskHandleId,
|
||||||
MaskSnapArgs,
|
MaskSnapArgs,
|
||||||
MaskSnapResult,
|
MaskSnapResult,
|
||||||
RectangleMaskParams,
|
RectangleMaskParams,
|
||||||
|
|
@ -19,13 +20,6 @@ import {
|
||||||
toGlobalMaskSnapLines,
|
toGlobalMaskSnapLines,
|
||||||
} from "./geometry";
|
} from "./geometry";
|
||||||
|
|
||||||
const CORNER_SIZE_HANDLES = new Set([
|
|
||||||
"top-left",
|
|
||||||
"top-right",
|
|
||||||
"bottom-left",
|
|
||||||
"bottom-right",
|
|
||||||
]);
|
|
||||||
|
|
||||||
function getClampedRatio({
|
function getClampedRatio({
|
||||||
next,
|
next,
|
||||||
base,
|
base,
|
||||||
|
|
@ -41,23 +35,31 @@ function getClampedRatio({
|
||||||
function getPreferredEdges({
|
function getPreferredEdges({
|
||||||
handleId,
|
handleId,
|
||||||
}: {
|
}: {
|
||||||
handleId: string;
|
handleId: MaskHandleId;
|
||||||
}): ScaleEdgePreference | undefined {
|
}): 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",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const preferredEdges = {
|
const preferredEdges = {
|
||||||
left:
|
left: false,
|
||||||
handleId === "left" ||
|
right: false,
|
||||||
handleId === "top-left" ||
|
top: false,
|
||||||
handleId === "bottom-left",
|
bottom: false,
|
||||||
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;
|
} satisfies ScaleEdgePreference;
|
||||||
|
|
||||||
return Object.values(preferredEdges).some(Boolean)
|
return Object.values(preferredEdges).some(Boolean)
|
||||||
|
|
@ -166,7 +168,7 @@ function snapBoxMaskSize({
|
||||||
canvasSize,
|
canvasSize,
|
||||||
snapThreshold,
|
snapThreshold,
|
||||||
}: {
|
}: {
|
||||||
handleId: string;
|
handleId: MaskHandleId;
|
||||||
startParams: RectangleMaskParams;
|
startParams: RectangleMaskParams;
|
||||||
proposedParams: RectangleMaskParams;
|
proposedParams: RectangleMaskParams;
|
||||||
bounds: ElementBounds;
|
bounds: ElementBounds;
|
||||||
|
|
@ -188,7 +190,10 @@ function snapBoxMaskSize({
|
||||||
Math.max(startParams.height, MIN_MASK_DIMENSION) * bounds.height;
|
Math.max(startParams.height, MIN_MASK_DIMENSION) * bounds.height;
|
||||||
const preferredEdges = getPreferredEdges({ handleId });
|
const preferredEdges = getPreferredEdges({ handleId });
|
||||||
|
|
||||||
if (handleId === "right" || handleId === "left") {
|
if (
|
||||||
|
handleId.kind === "edge" &&
|
||||||
|
(handleId.side === "right" || handleId.side === "left")
|
||||||
|
) {
|
||||||
const proposedScaleX = getClampedRatio({
|
const proposedScaleX = getClampedRatio({
|
||||||
next: proposedParams.width,
|
next: proposedParams.width,
|
||||||
base: startParams.width,
|
base: startParams.width,
|
||||||
|
|
@ -218,7 +223,10 @@ function snapBoxMaskSize({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleId === "top" || handleId === "bottom") {
|
if (
|
||||||
|
handleId.kind === "edge" &&
|
||||||
|
(handleId.side === "top" || handleId.side === "bottom")
|
||||||
|
) {
|
||||||
const proposedScaleY = getClampedRatio({
|
const proposedScaleY = getClampedRatio({
|
||||||
next: proposedParams.height,
|
next: proposedParams.height,
|
||||||
base: startParams.height,
|
base: startParams.height,
|
||||||
|
|
@ -251,7 +259,7 @@ function snapBoxMaskSize({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleId === "scale") {
|
if (handleId.kind === "scale") {
|
||||||
const baseScale = Math.max(startParams.scale, MIN_MASK_DIMENSION);
|
const baseScale = Math.max(startParams.scale, MIN_MASK_DIMENSION);
|
||||||
const proposedScale = getClampedRatio({
|
const proposedScale = getClampedRatio({
|
||||||
next: proposedParams.scale,
|
next: proposedParams.scale,
|
||||||
|
|
@ -281,7 +289,7 @@ function snapBoxMaskSize({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CORNER_SIZE_HANDLES.has(handleId)) {
|
if (handleId.kind === "corner") {
|
||||||
const proposedScale = getClampedRatio({
|
const proposedScale = getClampedRatio({
|
||||||
next: proposedParams.width,
|
next: proposedParams.width,
|
||||||
base: startParams.width,
|
base: startParams.width,
|
||||||
|
|
@ -322,14 +330,14 @@ export function snapBoxMaskInteraction({
|
||||||
canvasSize,
|
canvasSize,
|
||||||
snapThreshold,
|
snapThreshold,
|
||||||
}: {
|
}: {
|
||||||
handleId: string;
|
handleId: MaskHandleId;
|
||||||
startParams: RectangleMaskParams;
|
startParams: RectangleMaskParams;
|
||||||
proposedParams: RectangleMaskParams;
|
proposedParams: RectangleMaskParams;
|
||||||
bounds: ElementBounds;
|
bounds: ElementBounds;
|
||||||
canvasSize: { width: number; height: number };
|
canvasSize: { width: number; height: number };
|
||||||
snapThreshold: { x: number; y: number };
|
snapThreshold: { x: number; y: number };
|
||||||
}): MaskSnapResult<RectangleMaskParams> {
|
}): MaskSnapResult<RectangleMaskParams> {
|
||||||
if (handleId === "position") {
|
if (handleId.kind === "position") {
|
||||||
return snapMaskPosition({
|
return snapMaskPosition({
|
||||||
proposedParams,
|
proposedParams,
|
||||||
bounds,
|
bounds,
|
||||||
|
|
@ -338,7 +346,7 @@ export function snapBoxMaskInteraction({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleId === "rotation") {
|
if (handleId.kind === "rotation") {
|
||||||
return snapMaskRotation({ proposedParams });
|
return snapMaskRotation({ proposedParams });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -359,7 +367,7 @@ export function snapSplitMaskInteraction({
|
||||||
canvasSize,
|
canvasSize,
|
||||||
snapThreshold,
|
snapThreshold,
|
||||||
}: MaskSnapArgs<SplitMaskParams>): MaskSnapResult<SplitMaskParams> {
|
}: MaskSnapArgs<SplitMaskParams>): MaskSnapResult<SplitMaskParams> {
|
||||||
if (handleId === "position") {
|
if (handleId.kind === "position") {
|
||||||
return snapMaskPosition({
|
return snapMaskPosition({
|
||||||
proposedParams,
|
proposedParams,
|
||||||
bounds,
|
bounds,
|
||||||
|
|
@ -368,7 +376,7 @@ export function snapSplitMaskInteraction({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleId === "rotation") {
|
if (handleId.kind === "rotation") {
|
||||||
return snapMaskRotation({ proposedParams });
|
return snapMaskRotation({ proposedParams });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -187,8 +187,43 @@ export type MaskHandleIcon = "rotate" | "feather";
|
||||||
|
|
||||||
export type MaskHandleKind = "corner" | "edge" | "icon" | "point" | "tangent";
|
export type MaskHandleKind = "corner" | "edge" | "icon" | "point" | "tangent";
|
||||||
|
|
||||||
|
type Side = "left" | "right" | "top" | "bottom";
|
||||||
|
type CornerXY = { x: "left" | "right"; y: "top" | "bottom" };
|
||||||
|
type CustomTangent = "in" | "out";
|
||||||
|
|
||||||
|
export type MaskHandleId =
|
||||||
|
| { kind: "position" }
|
||||||
|
| { kind: "rotation" }
|
||||||
|
| { kind: "feather" }
|
||||||
|
| { kind: "scale" }
|
||||||
|
| { kind: "edge"; side: Side }
|
||||||
|
| { kind: "corner"; corner: CornerXY }
|
||||||
|
| { kind: "anchor"; pointId: string }
|
||||||
|
| { kind: "tangent"; pointId: string; side: CustomTangent }
|
||||||
|
| { kind: "segment"; index: number };
|
||||||
|
|
||||||
|
export function maskHandleIdKey({ id }: { id: MaskHandleId }): string {
|
||||||
|
switch (id.kind) {
|
||||||
|
case "position":
|
||||||
|
case "rotation":
|
||||||
|
case "feather":
|
||||||
|
case "scale":
|
||||||
|
return id.kind;
|
||||||
|
case "edge":
|
||||||
|
return id.side;
|
||||||
|
case "corner":
|
||||||
|
return `${id.corner.y}-${id.corner.x}`;
|
||||||
|
case "anchor":
|
||||||
|
return `point:${id.pointId}:anchor`;
|
||||||
|
case "tangent":
|
||||||
|
return `point:${id.pointId}:${id.side}`;
|
||||||
|
case "segment":
|
||||||
|
return `segment:${id.index}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface MaskHandlePosition {
|
export interface MaskHandlePosition {
|
||||||
id: string;
|
id: MaskHandleId;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
cursor: string;
|
cursor: string;
|
||||||
|
|
@ -205,7 +240,7 @@ export interface MaskLineOverlay {
|
||||||
start: { x: number; y: number };
|
start: { x: number; y: number };
|
||||||
end: { x: number; y: number };
|
end: { x: number; y: number };
|
||||||
cursor?: string;
|
cursor?: string;
|
||||||
handleId?: string;
|
handleId?: MaskHandleId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MaskRectOverlay {
|
export interface MaskRectOverlay {
|
||||||
|
|
@ -217,7 +252,7 @@ export interface MaskRectOverlay {
|
||||||
rotation: number;
|
rotation: number;
|
||||||
dashed?: boolean;
|
dashed?: boolean;
|
||||||
cursor?: string;
|
cursor?: string;
|
||||||
handleId?: string;
|
handleId?: MaskHandleId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MaskShapeOverlay {
|
export interface MaskShapeOverlay {
|
||||||
|
|
@ -229,7 +264,7 @@ export interface MaskShapeOverlay {
|
||||||
rotation: number;
|
rotation: number;
|
||||||
pathData: string;
|
pathData: string;
|
||||||
cursor?: string;
|
cursor?: string;
|
||||||
handleId?: string;
|
handleId?: MaskHandleId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MaskCanvasPathOverlay {
|
export interface MaskCanvasPathOverlay {
|
||||||
|
|
@ -238,7 +273,7 @@ export interface MaskCanvasPathOverlay {
|
||||||
pathData: string;
|
pathData: string;
|
||||||
coordinateSpace?: "canvas" | "overlay";
|
coordinateSpace?: "canvas" | "overlay";
|
||||||
cursor?: string;
|
cursor?: string;
|
||||||
handleId?: string;
|
handleId?: MaskHandleId;
|
||||||
strokeWidth?: number;
|
strokeWidth?: number;
|
||||||
strokeOpacity?: number;
|
strokeOpacity?: number;
|
||||||
}
|
}
|
||||||
|
|
@ -256,7 +291,7 @@ export interface MaskDefaultContext {
|
||||||
export interface MaskParamUpdateArgs<
|
export interface MaskParamUpdateArgs<
|
||||||
TParams extends BaseMaskParams = BaseMaskParams,
|
TParams extends BaseMaskParams = BaseMaskParams,
|
||||||
> {
|
> {
|
||||||
handleId: string;
|
handleId: MaskHandleId;
|
||||||
startParams: TParams;
|
startParams: TParams;
|
||||||
deltaX: number;
|
deltaX: number;
|
||||||
deltaY: number;
|
deltaY: number;
|
||||||
|
|
@ -267,7 +302,7 @@ export interface MaskParamUpdateArgs<
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MaskSnapArgs<TParams extends BaseMaskParams = BaseMaskParams> {
|
export interface MaskSnapArgs<TParams extends BaseMaskParams = BaseMaskParams> {
|
||||||
handleId: string;
|
handleId: MaskHandleId;
|
||||||
startParams: TParams;
|
startParams: TParams;
|
||||||
proposedParams: TParams;
|
proposedParams: TParams;
|
||||||
bounds: ElementBounds;
|
bounds: ElementBounds;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,8 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { usePreviewViewport } from "@/preview/components/preview-viewport";
|
import { usePreviewViewport } from "@/preview/components/preview-viewport";
|
||||||
import { useEditor } from "@/editor/use-editor";
|
import { useEditor } from "@/editor/use-editor";
|
||||||
import { useShiftKey } from "@/hooks/use-shift-key";
|
import { useShiftKey } from "@/hooks/use-shift-key";
|
||||||
import { masksRegistry } from "@/masks";
|
import { masksRegistry } from "@/masks";
|
||||||
import {
|
|
||||||
parseCustomMaskHandleId,
|
|
||||||
parseCustomMaskSegmentHandleId,
|
|
||||||
} from "@/masks/custom-path";
|
|
||||||
import { appendPointToCustomMask } from "@/masks/definitions/custom";
|
import { appendPointToCustomMask } from "@/masks/definitions/custom";
|
||||||
import {
|
import {
|
||||||
getVisibleElementsWithBounds,
|
getVisibleElementsWithBounds,
|
||||||
|
|
@ -17,14 +13,15 @@ import {
|
||||||
type SnapLine,
|
type SnapLine,
|
||||||
} from "@/preview/preview-snap";
|
} from "@/preview/preview-snap";
|
||||||
import type { SelectedMaskPointSelection } from "@/selection/editor-selection";
|
import type { SelectedMaskPointSelection } from "@/selection/editor-selection";
|
||||||
import type { Mask, MaskInteractionResult } from "@/masks/types";
|
import type { Mask, MaskHandleId, MaskInteractionResult } from "@/masks/types";
|
||||||
import type { MaskableElement } from "@/timeline";
|
import type { MaskableElement } from "@/timeline";
|
||||||
|
import { isMaskableElement } from "@/timeline/element-utils";
|
||||||
import { registerCanceller } from "@/editor/cancel-interaction";
|
import { registerCanceller } from "@/editor/cancel-interaction";
|
||||||
|
|
||||||
interface DragState {
|
interface DragState {
|
||||||
trackId: string;
|
trackId: string;
|
||||||
elementId: string;
|
elementId: string;
|
||||||
handleId: string;
|
handleId: MaskHandleId;
|
||||||
startCanvasX: number;
|
startCanvasX: number;
|
||||||
startCanvasY: number;
|
startCanvasY: number;
|
||||||
startParams: Mask["params"];
|
startParams: Mask["params"];
|
||||||
|
|
@ -99,12 +96,12 @@ export function useMaskHandles({
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
const isShiftHeldRef = useShiftKey();
|
const isShiftHeldRef = useShiftKey();
|
||||||
const viewport = usePreviewViewport();
|
const viewport = usePreviewViewport();
|
||||||
const [activeHandleId, setActiveHandleId] = useState<string | null>(null);
|
const [activeHandleId, setActiveHandleId] = useState<MaskHandleId | null>(null);
|
||||||
const dragStateRef = useRef<DragState | null>(null);
|
const dragStateRef = useRef<DragState | null>(null);
|
||||||
const pendingSegmentInsertRef = useRef<PendingSegmentInsertState | null>(
|
const pendingSegmentInsertRef = useRef<PendingSegmentInsertState | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const captureRef = useRef<{ element: HTMLElement; pointerId: number } | null>(
|
const captureRef = useRef<{ element: Element; pointerId: number } | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -137,7 +134,8 @@ export function useMaskHandles({
|
||||||
item.trackId === sel.trackId && item.elementId === sel.elementId,
|
item.trackId === sel.trackId && item.elementId === sel.elementId,
|
||||||
);
|
);
|
||||||
if (!entry) return null;
|
if (!entry) return null;
|
||||||
const element = entry.element as MaskableElement;
|
if (!isMaskableElement(entry.element)) return null;
|
||||||
|
const element = entry.element;
|
||||||
const masks = element.masks ?? [];
|
const masks = element.masks ?? [];
|
||||||
if (masks.length === 0) return null;
|
if (masks.length === 0) return null;
|
||||||
const activeMaskId = masks.some((mask) =>
|
const activeMaskId = masks.some((mask) =>
|
||||||
|
|
@ -186,28 +184,27 @@ export function useMaskHandles({
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePositions = baseHandlePositions.map((handle) => {
|
const handlePositions = baseHandlePositions.map((handle) => {
|
||||||
const parsedHandle = parseCustomMaskHandleId({ handleId: handle.id });
|
if (handle.id.kind !== "anchor") {
|
||||||
if (!parsedHandle || parsedHandle.part !== "anchor") {
|
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...handle,
|
...handle,
|
||||||
isSelected: selectedPointIds.has(parsedHandle.pointId),
|
isSelected: selectedPointIds.has(handle.id.pointId),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const customMaskPointIds =
|
const customMaskPointIds = useMemo(
|
||||||
selectedWithMask?.mask.type === "custom"
|
() =>
|
||||||
? handlePositions
|
selectedWithMask?.mask.type === "custom"
|
||||||
.filter((h) => h.kind === "point")
|
? handlePositions
|
||||||
.map((h) => {
|
.filter((h) => h.kind === "point")
|
||||||
const parsedHandle = parseCustomMaskHandleId({ handleId: h.id });
|
.map((h) => {
|
||||||
return parsedHandle?.part === "anchor"
|
return h.id.kind === "anchor" ? h.id.pointId : null;
|
||||||
? parsedHandle.pointId
|
})
|
||||||
: null;
|
.filter((id): id is string => id !== null)
|
||||||
})
|
: [],
|
||||||
.filter((id): id is string => id !== null)
|
[handlePositions, selectedWithMask?.mask.type],
|
||||||
: [];
|
);
|
||||||
const isCreatingCustomMask =
|
const isCreatingCustomMask =
|
||||||
selectedWithMask?.mask.type === "custom" &&
|
selectedWithMask?.mask.type === "custom" &&
|
||||||
(!selectedWithMask.mask.params.closed || customMaskPointIds.length === 0);
|
(!selectedWithMask.mask.params.closed || customMaskPointIds.length === 0);
|
||||||
|
|
@ -339,23 +336,30 @@ export function useMaskHandles({
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handlePointerDown = useCallback(
|
const handlePointerDown = useCallback(
|
||||||
({ event, handleId }: { event: React.PointerEvent; handleId: string }) => {
|
({
|
||||||
|
event,
|
||||||
|
handleId,
|
||||||
|
}: {
|
||||||
|
event: React.PointerEvent;
|
||||||
|
handleId: MaskHandleId;
|
||||||
|
}) => {
|
||||||
if (!selectedWithMask) return;
|
if (!selectedWithMask) return;
|
||||||
if (event.button !== 0) return;
|
if (event.button !== 0) return;
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const parsedHandle =
|
const anchorHandle =
|
||||||
selectedWithMask.mask.type === "custom"
|
selectedWithMask.mask.type === "custom" && handleId.kind === "anchor"
|
||||||
? parseCustomMaskHandleId({ handleId })
|
? handleId
|
||||||
: null;
|
: null;
|
||||||
const parsedSegmentHandle =
|
const segmentHandle =
|
||||||
selectedWithMask.mask.type === "custom"
|
selectedWithMask.mask.type === "custom" && handleId.kind === "segment"
|
||||||
? parseCustomMaskSegmentHandleId({ handleId })
|
? handleId
|
||||||
: null;
|
: null;
|
||||||
if (isCreatingCustomMask) {
|
if (isCreatingCustomMask) {
|
||||||
const firstPointId = customMaskPointIds[0];
|
const firstPointId = customMaskPointIds[0];
|
||||||
if (
|
if (
|
||||||
firstPointId &&
|
firstPointId &&
|
||||||
handleId === `point:${firstPointId}:anchor` &&
|
handleId.kind === "anchor" &&
|
||||||
|
handleId.pointId === firstPointId &&
|
||||||
customMaskPointIds.length >= 3 &&
|
customMaskPointIds.length >= 3 &&
|
||||||
selectedWithMask.mask.type === "custom"
|
selectedWithMask.mask.type === "custom"
|
||||||
) {
|
) {
|
||||||
|
|
@ -390,13 +394,13 @@ export function useMaskHandles({
|
||||||
});
|
});
|
||||||
if (!pos) return;
|
if (!pos) return;
|
||||||
|
|
||||||
if (parsedSegmentHandle && selectedWithMask.mask.type === "custom") {
|
if (segmentHandle && selectedWithMask.mask.type === "custom") {
|
||||||
setActiveHandleId(handleId);
|
setActiveHandleId(handleId);
|
||||||
pendingSegmentInsertRef.current = {
|
pendingSegmentInsertRef.current = {
|
||||||
trackId: selectedWithMask.trackId,
|
trackId: selectedWithMask.trackId,
|
||||||
elementId: selectedWithMask.elementId,
|
elementId: selectedWithMask.elementId,
|
||||||
maskId: selectedWithMask.mask.id,
|
maskId: selectedWithMask.mask.id,
|
||||||
segmentIndex: parsedSegmentHandle.segmentIndex,
|
segmentIndex: segmentHandle.index,
|
||||||
startClientX: event.clientX,
|
startClientX: event.clientX,
|
||||||
startClientY: event.clientY,
|
startClientY: event.clientY,
|
||||||
startCanvasX: pos.x,
|
startCanvasX: pos.x,
|
||||||
|
|
@ -404,7 +408,7 @@ export function useMaskHandles({
|
||||||
startParams: { ...selectedWithMask.mask.params },
|
startParams: { ...selectedWithMask.mask.params },
|
||||||
bounds: selectedWithMask.bounds,
|
bounds: selectedWithMask.bounds,
|
||||||
};
|
};
|
||||||
const captureTarget = event.currentTarget as HTMLElement;
|
const captureTarget = event.currentTarget;
|
||||||
captureTarget.setPointerCapture(event.pointerId);
|
captureTarget.setPointerCapture(event.pointerId);
|
||||||
captureRef.current = {
|
captureRef.current = {
|
||||||
element: captureTarget,
|
element: captureTarget,
|
||||||
|
|
@ -413,9 +417,9 @@ export function useMaskHandles({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedHandle?.part === "anchor") {
|
if (anchorHandle) {
|
||||||
updateCustomMaskPointSelection({
|
updateCustomMaskPointSelection({
|
||||||
pointId: parsedHandle.pointId,
|
pointId: anchorHandle.pointId,
|
||||||
toggleSelection: event.shiftKey,
|
toggleSelection: event.shiftKey,
|
||||||
});
|
});
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
|
|
@ -434,7 +438,7 @@ export function useMaskHandles({
|
||||||
startParams: { ...selectedWithMask.mask.params },
|
startParams: { ...selectedWithMask.mask.params },
|
||||||
};
|
};
|
||||||
setActiveHandleId(handleId);
|
setActiveHandleId(handleId);
|
||||||
const captureTarget = event.currentTarget as HTMLElement;
|
const captureTarget = event.currentTarget;
|
||||||
captureTarget.setPointerCapture(event.pointerId);
|
captureTarget.setPointerCapture(event.pointerId);
|
||||||
captureRef.current = {
|
captureRef.current = {
|
||||||
element: captureTarget,
|
element: captureTarget,
|
||||||
|
|
@ -513,13 +517,13 @@ export function useMaskHandles({
|
||||||
dragStateRef.current = {
|
dragStateRef.current = {
|
||||||
trackId: pendingSegmentInsert.trackId,
|
trackId: pendingSegmentInsert.trackId,
|
||||||
elementId: pendingSegmentInsert.elementId,
|
elementId: pendingSegmentInsert.elementId,
|
||||||
handleId: "position",
|
handleId: { kind: "position" },
|
||||||
startCanvasX: pendingSegmentInsert.startCanvasX,
|
startCanvasX: pendingSegmentInsert.startCanvasX,
|
||||||
startCanvasY: pendingSegmentInsert.startCanvasY,
|
startCanvasY: pendingSegmentInsert.startCanvasY,
|
||||||
startParams: pendingSegmentInsert.startParams,
|
startParams: pendingSegmentInsert.startParams,
|
||||||
};
|
};
|
||||||
pendingSegmentInsertRef.current = null;
|
pendingSegmentInsertRef.current = null;
|
||||||
setActiveHandleId("position");
|
setActiveHandleId({ kind: "position" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { PEN_CURSOR } from "@/preview/components/cursors";
|
import { PEN_CURSOR } from "@/preview/components/cursors";
|
||||||
import { usePreviewViewport } from "@/preview/components/preview-viewport";
|
import { usePreviewViewport } from "@/preview/components/preview-viewport";
|
||||||
import { useMaskHandles } from "@/masks/use-mask-handles";
|
import { useMaskHandles } from "@/masks/use-mask-handles";
|
||||||
|
import { maskHandleIdKey, type MaskHandleId } from "@/masks/types";
|
||||||
import type { SnapLine } from "@/preview/preview-snap";
|
import type { SnapLine } from "@/preview/preview-snap";
|
||||||
import {
|
import {
|
||||||
CornerHandle,
|
CornerHandle,
|
||||||
|
|
@ -72,7 +73,7 @@ export function MaskHandles({
|
||||||
handleId,
|
handleId,
|
||||||
}: {
|
}: {
|
||||||
event: React.PointerEvent;
|
event: React.PointerEvent;
|
||||||
handleId: string;
|
handleId: MaskHandleId;
|
||||||
}) => {
|
}) => {
|
||||||
if (viewport.handlePanPointerDown({ event })) {
|
if (viewport.handlePanPointerDown({ event })) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -193,11 +194,12 @@ export function MaskHandles({
|
||||||
})}
|
})}
|
||||||
{handlePositions.map((handle) => {
|
{handlePositions.map((handle) => {
|
||||||
const screen = toOverlay({ canvasX: handle.x, canvasY: handle.y });
|
const screen = toOverlay({ canvasX: handle.x, canvasY: handle.y });
|
||||||
|
const key = maskHandleIdKey({ id: handle.id });
|
||||||
|
|
||||||
if (handle.kind === "icon" && handle.icon === "rotate") {
|
if (handle.kind === "icon" && handle.icon === "rotate") {
|
||||||
return (
|
return (
|
||||||
<IconHandle
|
<IconHandle
|
||||||
key={handle.id}
|
key={key}
|
||||||
icon={Rotate01Icon}
|
icon={Rotate01Icon}
|
||||||
screen={screen}
|
screen={screen}
|
||||||
onPointerDown={(event) =>
|
onPointerDown={(event) =>
|
||||||
|
|
@ -212,7 +214,7 @@ export function MaskHandles({
|
||||||
if (handle.kind === "icon" && handle.icon === "feather") {
|
if (handle.kind === "icon" && handle.icon === "feather") {
|
||||||
return (
|
return (
|
||||||
<IconHandle
|
<IconHandle
|
||||||
key={handle.id}
|
key={key}
|
||||||
icon={FeatherIcon}
|
icon={FeatherIcon}
|
||||||
screen={screen}
|
screen={screen}
|
||||||
onPointerDown={(event) =>
|
onPointerDown={(event) =>
|
||||||
|
|
@ -227,7 +229,7 @@ export function MaskHandles({
|
||||||
if (handle.kind === "edge" && handle.edgeAxis === "horizontal") {
|
if (handle.kind === "edge" && handle.edgeAxis === "horizontal") {
|
||||||
return (
|
return (
|
||||||
<EdgeHandle
|
<EdgeHandle
|
||||||
key={handle.id}
|
key={key}
|
||||||
edge="right"
|
edge="right"
|
||||||
screen={screen}
|
screen={screen}
|
||||||
rotation={handle.rotation ?? 0}
|
rotation={handle.rotation ?? 0}
|
||||||
|
|
@ -243,7 +245,7 @@ export function MaskHandles({
|
||||||
if (handle.kind === "edge" && handle.edgeAxis === "vertical") {
|
if (handle.kind === "edge" && handle.edgeAxis === "vertical") {
|
||||||
return (
|
return (
|
||||||
<EdgeHandle
|
<EdgeHandle
|
||||||
key={handle.id}
|
key={key}
|
||||||
edge="bottom"
|
edge="bottom"
|
||||||
screen={screen}
|
screen={screen}
|
||||||
rotation={handle.rotation ?? 0}
|
rotation={handle.rotation ?? 0}
|
||||||
|
|
@ -259,7 +261,7 @@ export function MaskHandles({
|
||||||
if (handle.kind === "point" || handle.kind === "tangent") {
|
if (handle.kind === "point" || handle.kind === "tangent") {
|
||||||
return (
|
return (
|
||||||
<CircleHandle
|
<CircleHandle
|
||||||
key={handle.id}
|
key={key}
|
||||||
screen={screen}
|
screen={screen}
|
||||||
size={
|
size={
|
||||||
handle.kind === "tangent"
|
handle.kind === "tangent"
|
||||||
|
|
@ -279,7 +281,7 @@ export function MaskHandles({
|
||||||
if (handle.kind === "corner") {
|
if (handle.kind === "corner") {
|
||||||
return (
|
return (
|
||||||
<CornerHandle
|
<CornerHandle
|
||||||
key={handle.id}
|
key={key}
|
||||||
screen={screen}
|
screen={screen}
|
||||||
onPointerDown={(event) =>
|
onPointerDown={(event) =>
|
||||||
handleMaskPointerDown({ event, handleId: handle.id })
|
handleMaskPointerDown({ event, handleId: handle.id })
|
||||||
|
|
@ -292,7 +294,7 @@ export function MaskHandles({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CornerHandle
|
<CornerHandle
|
||||||
key={handle.id}
|
key={key}
|
||||||
cursor={handle.cursor}
|
cursor={handle.cursor}
|
||||||
screen={screen}
|
screen={screen}
|
||||||
onPointerDown={(event) =>
|
onPointerDown={(event) =>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue