import type { ArtboardId } from "@core/id"; import type { Vec2D } from "@core/geometry"; import type { Command } from "./command"; import { commandIds } from "./ids"; export type ViewportPanPayload = { delta: Vec2D; }; export type ViewportSetZoomPayload = { zoom: number; }; export type ViewportZoomAroundPointPayload = { zoom: number; point: Vec2D; }; export type ViewportSetSizePayload = { w: number; h: number; }; export type ViewportFitArtboardPayload = { artboardId?: ArtboardId; padding?: number; }; export const viewportPanCommand: Command = { id: commandIds.viewportPan, name: "Pan viewport", execute({ state }, payload) { return { ...state, editor: { ...state.editor, viewport: { ...state.editor.viewport, center: { x: state.editor.viewport.center.x + payload.delta.x, y: state.editor.viewport.center.y + payload.delta.y, }, }, }, }; }, }; export const viewportSetZoomCommand: Command = { id: commandIds.viewportSetZoom, name: "Set viewport zoom", execute({ state }, payload) { const zoom = Math.max(0.01, payload.zoom); return { ...state, editor: { ...state.editor, viewport: { ...state.editor.viewport, zoom, }, }, }; }, }; export const viewportZoomAroundPointCommand: Command = { id: commandIds.viewportZoomAroundPoint, name: "Zoom viewport around point", execute({ state }, payload) { const viewport = state.editor.viewport; const zoom = Math.max(0.01, payload.zoom); const offset = { x: payload.point.x - viewport.size.w / 2, y: payload.point.y - viewport.size.h / 2, }; const documentPoint = { x: viewport.center.x + offset.x / viewport.zoom, y: viewport.center.y + offset.y / viewport.zoom, }; return { ...state, editor: { ...state.editor, viewport: { ...viewport, zoom, center: { x: documentPoint.x - offset.x / zoom, y: documentPoint.y - offset.y / zoom, }, }, }, }; }, }; export const viewportSetSizeCommand: Command = { id: commandIds.viewportSetSize, name: "Set viewport size", execute({ state }, payload) { return { ...state, editor: { ...state.editor, viewport: { ...state.editor.viewport, size: { w: Math.max(0, payload.w), h: Math.max(0, payload.h), }, }, }, }; }, }; export const viewportFitArtboardCommand: Command = { id: commandIds.viewportFitArtboard, name: "Fit artboard in viewport", execute({ state }, payload) { const artboardId = payload?.artboardId ?? state.editor.selection.artboardId; const artboard = artboardId ? state.document.artboards.find((candidate) => candidate.id === artboardId) : state.document.artboards[0]; if (!artboard) return state; const padding = Math.max(0, payload?.padding ?? 48); const availableWidth = Math.max(0, state.editor.viewport.size.w - padding * 2); const availableHeight = Math.max(0, state.editor.viewport.size.h - padding * 2); const canFitZoom = availableWidth > 0 && availableHeight > 0 && artboard.bounds.w > 0 && artboard.bounds.h > 0; const zoom = canFitZoom ? Math.max(0.01, Math.min(availableWidth / artboard.bounds.w, availableHeight / artboard.bounds.h)) : state.editor.viewport.zoom; return { ...state, editor: { ...state.editor, viewport: { ...state.editor.viewport, center: { x: artboard.bounds.x + artboard.bounds.w / 2, y: artboard.bounds.y + artboard.bounds.h / 2, }, zoom, }, }, }; }, }; export const viewportResetCommand: Command = { id: commandIds.viewportReset, name: "Reset viewport", execute({ state }) { return { ...state, editor: { ...state.editor, viewport: { center: { x: 0, y: 0 }, zoom: 1, rotation: 0, size: state.editor.viewport.size, }, }, }; }, }; export const viewportCommands = [ viewportPanCommand, viewportSetZoomCommand, viewportZoomAroundPointCommand, viewportSetSizeCommand, viewportFitArtboardCommand, viewportResetCommand, ] satisfies Command[];