import type { Rect, Vec2D } from "@core/geometry"; import { applyTransformTargetBounds } from "@editor/transform-targets"; import type { TransformHandle, TransformTarget } from "@editor/transform"; import type { Command } from "./command"; import { commandIds } from "./ids"; export type TransformBeginPayload = { target: TransformTarget; handle: TransformHandle; point: Vec2D; initialBounds: Rect; }; export type TransformUpdatePayload = { point: Vec2D; shiftKey?: boolean; }; export type TransformSetBoundsPayload = { target: TransformTarget; bounds: Rect; }; export const transformBeginCommand: Command = { id: commandIds.transformBegin, name: "Begin transform", execute({ state }, payload) { return { ...state, editor: { ...state.editor, transformSession: { target: payload.target, handle: payload.handle, startPoint: payload.point, initialBounds: payload.initialBounds, }, }, }; }, }; export const transformUpdateCommand: Command = { id: commandIds.transformUpdate, name: "Update transform", execute({ state }, payload) { const session = state.editor.transformSession; if (!session) return state; const nextBounds = transformBounds( session.initialBounds, session.handle, { x: payload.point.x - session.startPoint.x, y: payload.point.y - session.startPoint.y, }, payload.shiftKey === true, ); return { ...state, document: applyTransformTargetBounds(state.document, session.target, nextBounds), }; }, }; export const transformSetBoundsCommand: Command = { id: commandIds.transformSetBounds, name: "Set transform bounds", execute({ state }, payload) { return { ...state, document: applyTransformTargetBounds(state.document, payload.target, normalizeRect(payload.bounds)), }; }, }; export const transformEndCommand: Command = { id: commandIds.transformEnd, name: "End transform", execute({ state }) { if (!state.editor.transformSession) return state; return { ...state, editor: { ...state.editor, transformSession: undefined, }, }; }, }; export const transformCommands = [transformBeginCommand, transformUpdateCommand, transformSetBoundsCommand, transformEndCommand] satisfies Command[]; function transformBounds(bounds: Rect, handle: TransformHandle, delta: Vec2D, constrained = false): Rect { if (handle === "body") { const constrainedDelta = constrained ? constrainMoveDelta(delta) : delta; return { ...bounds, x: bounds.x + constrainedDelta.x, y: bounds.y + constrainedDelta.y }; } let x = bounds.x; let y = bounds.y; let w = bounds.w; let h = bounds.h; if (handle.includes("w")) { x = bounds.x + delta.x; w = bounds.w - delta.x; } if (handle.includes("e")) w = bounds.w + delta.x; if (handle.includes("n")) { y = bounds.y + delta.y; h = bounds.h - delta.y; } if (handle.includes("s")) h = bounds.h + delta.y; const resized = normalizeRect({ x, y, w, h }); if (!constrained || !isCornerHandle(handle)) return resized; return proportionalCornerResize(bounds, handle, resized); } function constrainMoveDelta(delta: Vec2D): Vec2D { return Math.abs(delta.x) >= Math.abs(delta.y) ? { x: delta.x, y: 0 } : { x: 0, y: delta.y }; } function isCornerHandle(handle: TransformHandle) { return handle.length === 2; } function proportionalCornerResize(initial: Rect, handle: TransformHandle, resized: Rect): Rect { const aspectRatio = initial.w / initial.h; const widthScale = resized.w / initial.w; const heightScale = resized.h / initial.h; const scale = Math.abs(widthScale - 1) >= Math.abs(heightScale - 1) ? widthScale : heightScale; const w = Math.max(1, initial.w * scale); const h = Math.max(1, w / aspectRatio); const x = handle.includes("w") ? initial.x + initial.w - w : initial.x; const y = handle.includes("n") ? initial.y + initial.h - h : initial.y; return { x, y, w, h }; } function normalizeRect(rect: Rect): Rect { const minSize = 1; return { x: rect.w < minSize ? rect.x + rect.w - minSize : rect.x, y: rect.h < minSize ? rect.y + rect.h - minSize : rect.y, w: Math.max(minSize, rect.w), h: Math.max(minSize, rect.h), }; }