129 lines
4.9 KiB
TypeScript
129 lines
4.9 KiB
TypeScript
import { commandIds } from "@commands/ids";
|
|
import type { Dispatch } from "@commands/dispatcher";
|
|
import type { ImageDocument } from "@core/document";
|
|
import type { Rect, Vec2D } from "@core/geometry";
|
|
import {
|
|
documentRectToViewportRect,
|
|
resolveTransformTargetBounds,
|
|
selectedTransformTarget,
|
|
viewportPointToDocumentPoint,
|
|
type InputSelectionState,
|
|
type InputTransformTarget,
|
|
type InputViewportState,
|
|
} from "./document-geometry";
|
|
import { findLayerInfoInDocument } from "./layers-panel";
|
|
import type { PointerInputEvent } from "./pointer";
|
|
|
|
type TransformHandle = "body" | "nw" | "n" | "ne" | "e" | "se" | "s" | "sw" | "w";
|
|
|
|
type InputToolId = "select" | "brush" | "eraser" | "chromaKey" | "pan";
|
|
|
|
type InputInteractionMode =
|
|
| { type: "tool"; tool: InputToolId }
|
|
| { type: "temporary-pan"; previousTool: InputToolId };
|
|
|
|
export type TransformControlsEditorState = {
|
|
viewport: InputViewportState;
|
|
selection: InputSelectionState;
|
|
tools: {
|
|
activeTool: InputToolId;
|
|
interactionMode: InputInteractionMode;
|
|
};
|
|
transformSession?: unknown;
|
|
};
|
|
|
|
export type TransformControlsInputController = {
|
|
pointerDown(event: PointerInputEvent): boolean;
|
|
pointerMove(event: PointerInputEvent): boolean;
|
|
pointerUp(event: PointerInputEvent): boolean;
|
|
};
|
|
|
|
export function createTransformControlsInputController(options: {
|
|
getDocument: () => ImageDocument;
|
|
getEditor: () => TransformControlsEditorState;
|
|
dispatch: Dispatch;
|
|
}): TransformControlsInputController {
|
|
return {
|
|
pointerDown(event) {
|
|
if (event.pointerType !== "mouse" || (event.buttons & 1) !== 1) return false;
|
|
|
|
const editor = options.getEditor();
|
|
if (editor.tools.activeTool !== "select" || isPanInteractionMode(editor.tools.interactionMode)) return false;
|
|
|
|
const document = options.getDocument();
|
|
const target = selectedTransformTarget(document, editor.selection);
|
|
if (!target || isTransformTargetLocked(document, target)) return false;
|
|
|
|
const bounds = resolveTransformTargetBounds(document, target);
|
|
if (!bounds) return false;
|
|
|
|
const handle = hitTestArtboardTransformHandle(event.position, bounds, editor.viewport);
|
|
if (!handle) return false;
|
|
|
|
options.dispatch(commandIds.transformBegin, {
|
|
target,
|
|
handle,
|
|
point: viewportPointToDocumentPoint(event.position, editor.viewport),
|
|
initialBounds: bounds,
|
|
});
|
|
return true;
|
|
},
|
|
pointerMove(event) {
|
|
const editor = options.getEditor();
|
|
if (!editor.transformSession || isPanInteractionMode(editor.tools.interactionMode)) return false;
|
|
|
|
options.dispatch(commandIds.transformUpdate, { point: viewportPointToDocumentPoint(event.position, editor.viewport), shiftKey: event.shiftKey });
|
|
return true;
|
|
},
|
|
pointerUp() {
|
|
if (!options.getEditor().transformSession) return false;
|
|
|
|
options.dispatch(commandIds.transformEnd, undefined);
|
|
return true;
|
|
},
|
|
};
|
|
}
|
|
|
|
export function hitTestArtboardTransformHandle(position: Vec2D, bounds: Rect, viewport: InputViewportState): TransformHandle | undefined {
|
|
const rect = documentRectToViewportRect(bounds, viewport);
|
|
const handles = transformHandleRects(rect);
|
|
const handle = handles.find((candidate) => pointInRect(position, candidate.rect));
|
|
if (handle) return handle.handle;
|
|
if (pointInRect(position, rect)) return "body";
|
|
return undefined;
|
|
}
|
|
|
|
function isTransformTargetLocked(document: ImageDocument, target: InputTransformTarget) {
|
|
if (target.type === "artboard") {
|
|
const artboard = document.artboards.find((candidate) => candidate.id === target.id);
|
|
return !artboard || !artboard.visible || artboard.locked;
|
|
}
|
|
const layer = findLayerInfoInDocument(document, target.id)?.layer;
|
|
return !layer || !layer.visible || layer.locked;
|
|
}
|
|
|
|
function transformHandleRects(rect: Rect): { handle: TransformHandle; rect: Rect }[] {
|
|
const size = 12;
|
|
const half = size / 2;
|
|
const points: { handle: TransformHandle; point: Vec2D }[] = [
|
|
{ handle: "nw", point: { x: rect.x, y: rect.y } },
|
|
{ handle: "n", point: { x: rect.x + rect.w / 2, y: rect.y } },
|
|
{ handle: "ne", point: { x: rect.x + rect.w, y: rect.y } },
|
|
{ handle: "e", point: { x: rect.x + rect.w, y: rect.y + rect.h / 2 } },
|
|
{ handle: "se", point: { x: rect.x + rect.w, y: rect.y + rect.h } },
|
|
{ handle: "s", point: { x: rect.x + rect.w / 2, y: rect.y + rect.h } },
|
|
{ handle: "sw", point: { x: rect.x, y: rect.y + rect.h } },
|
|
{ handle: "w", point: { x: rect.x, y: rect.y + rect.h / 2 } },
|
|
];
|
|
|
|
return points.map(({ handle, point }) => ({ handle, rect: { x: point.x - half, y: point.y - half, w: size, h: size } }));
|
|
}
|
|
|
|
function isPanInteractionMode(interactionMode: InputInteractionMode): boolean {
|
|
return interactionMode.type === "temporary-pan" || (interactionMode.type === "tool" && interactionMode.tool === "pan");
|
|
}
|
|
|
|
function pointInRect(point: Vec2D, rect: Rect) {
|
|
return point.x >= rect.x && point.x <= rect.x + rect.w && point.y >= rect.y && point.y <= rect.y + rect.h;
|
|
}
|