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 type { EditorState } from "@editor/state"; import { resolveTransformTargetBounds, selectedTransformTarget } from "@editor/transform-targets"; import type { TransformHandle } from "@editor/transform"; import type { PointerInputEvent } from "./pointer"; export type TransformControlsInputController = { pointerDown(event: PointerInputEvent): boolean; pointerMove(event: PointerInputEvent): boolean; pointerUp(event: PointerInputEvent): boolean; }; export function createTransformControlsInputController(options: { getDocument: () => ImageDocument; getEditor: () => EditorState; 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") return false; const document = options.getDocument(); const target = selectedTransformTarget(document, editor.selection); if (!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) return false; options.dispatch(commandIds.transformUpdate, { point: viewportPointToDocumentPoint(event.position, editor.viewport) }); return true; }, pointerUp() { if (!options.getEditor().transformSession) return false; options.dispatch(commandIds.transformEnd, undefined); return true; }, }; } export function hitTestArtboardTransformHandle(position: Vec2D, bounds: Rect, viewport: EditorState["viewport"]): 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 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 viewportPointToDocumentPoint(point: Vec2D, viewport: EditorState["viewport"]): Vec2D { return { x: viewport.center.x + (point.x - viewport.size.w / 2) / viewport.zoom, y: viewport.center.y + (point.y - viewport.size.h / 2) / viewport.zoom, }; } function documentRectToViewportRect(rect: Rect, viewport: EditorState["viewport"]): Rect { return { x: viewport.size.w / 2 + (rect.x - viewport.center.x) * viewport.zoom, y: viewport.size.h / 2 + (rect.y - viewport.center.y) * viewport.zoom, w: rect.w * viewport.zoom, h: rect.h * viewport.zoom, }; } 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; }