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" | "generate" | "brush" | "eraser" | "chromaKey" | "magicWand" | "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; }