import type { ImageDocument } from "@core/document"; import type { Vec2D } from "@core/geometry"; import type { LayerId, ArtboardId, AssetId } from "@core/id"; import type { Layer } from "@core/layer"; import type { MaskViewMode } from "@editor/state"; import type { BrushSettings, ToolId } from "@editor/tools"; import type { Command } from "./command"; import { commandIds } from "./ids"; export type ToolSetActivePayload = { tool: ToolId; }; export type ToolSetBrushSettingsPayload = Partial; export type ToolSetBrushPreviewPayload = { position: Vec2D } | undefined; export type ToolSetBrushStrokePreviewPayload = | { layerId: LayerId; assetId: AssetId; source: string; } | undefined; export type ToolSetMaskViewModePayload = { mode: MaskViewMode; }; export type ToolEnterMaskEditPayload = { targetLayerId: LayerId; maskLayerId: LayerId; }; export const toolSetActiveCommand: Command = { id: commandIds.toolSetActive, name: "Set active tool", execute({ state }, payload) { return { ...state, editor: { ...state.editor, tools: { ...state.editor.tools, activeTool: payload.tool, interactionMode: { type: "tool", tool: payload.tool }, }, brushPreview: undefined, brushStrokePreview: undefined, }, }; }, }; export const toolSetBrushSettingsCommand: Command = { id: commandIds.toolSetBrushSettings, name: "Set brush settings", execute({ state }, payload) { return { ...state, editor: { ...state.editor, tools: { ...state.editor.tools, brush: { color: payload.color ?? state.editor.tools.brush.color, size: clampNumber(payload.size ?? state.editor.tools.brush.size, 1, 200), hardness: clampNumber(payload.hardness ?? state.editor.tools.brush.hardness, 0, 100), }, }, }, }; }, }; export const toolSetBrushPreviewCommand: Command = { id: commandIds.toolSetBrushPreview, name: "Set brush preview", execute({ state }, payload) { if (!payload) { if (!state.editor.brushPreview) return state; return { ...state, editor: { ...state.editor, brushPreview: undefined } }; } const currentPosition = state.editor.brushPreview?.position; if (currentPosition && currentPosition.x === payload.position.x && currentPosition.y === payload.position.y) return state; return { ...state, editor: { ...state.editor, brushPreview: { position: { ...payload.position } }, }, }; }, }; export const toolSetBrushStrokePreviewCommand: Command = { id: commandIds.toolSetBrushStrokePreview, name: "Set brush stroke preview", execute({ state }, payload) { if (!payload) { if (!state.editor.brushStrokePreview) return state; return { ...state, editor: { ...state.editor, brushStrokePreview: undefined } }; } const currentPreview = state.editor.brushStrokePreview; if (currentPreview?.layerId === payload.layerId && currentPreview.assetId === payload.assetId && currentPreview.source === payload.source) return state; return { ...state, editor: { ...state.editor, brushStrokePreview: { layerId: payload.layerId, assetId: payload.assetId, source: payload.source }, }, }; }, }; export const toolSetMaskViewModeCommand: Command = { id: commandIds.toolSetMaskViewMode, name: "Set mask view mode", execute({ state }, payload) { if (!state.editor.maskEdit || state.editor.maskEdit.viewMode === payload.mode) return state; return { ...state, editor: { ...state.editor, maskEdit: { ...state.editor.maskEdit, viewMode: payload.mode }, }, }; }, }; export const toolEnterMaskEditCommand: Command = { id: commandIds.toolEnterMaskEdit, name: "Enter mask edit", execute({ state }, payload) { const targetLocation = findLayerLocation(state.document, payload.targetLayerId); const maskLocation = findLayerLocation(state.document, payload.maskLayerId); if (!targetLocation || !maskLocation) return state; if (targetLocation.layer.clippingMask?.maskLayerId !== payload.maskLayerId) return state; if (maskLocation.layer.type === "group") return state; return { ...state, editor: { ...state.editor, selection: { artboardId: targetLocation.artboardId, layerIds: [payload.targetLayerId] }, maskEdit: { targetLayerId: payload.targetLayerId, maskLayerId: payload.maskLayerId }, brushPreview: undefined, brushStrokePreview: undefined, tools: { ...state.editor.tools, activeTool: "brush", interactionMode: { type: "tool", tool: "brush" }, }, }, }; }, }; export const toolExitMaskEditCommand: Command = { id: commandIds.toolExitMaskEdit, name: "Exit mask edit", execute({ state }) { if (!state.editor.maskEdit) return state; return { ...state, editor: { ...state.editor, maskEdit: undefined, brushPreview: undefined, brushStrokePreview: undefined, }, }; }, }; export const toolEnterTemporaryPanCommand: Command = { id: commandIds.toolEnterTemporaryPan, name: "Enter temporary pan", execute({ state }) { if (state.editor.tools.interactionMode.type === "temporary-pan") return state; return { ...state, editor: { ...state.editor, tools: { ...state.editor.tools, interactionMode: { type: "temporary-pan", previousTool: state.editor.tools.activeTool }, }, }, }; }, }; export const toolExitTemporaryPanCommand: Command = { id: commandIds.toolExitTemporaryPan, name: "Exit temporary pan", execute({ state }) { const mode = state.editor.tools.interactionMode; if (mode.type !== "temporary-pan") return state; return { ...state, editor: { ...state.editor, tools: { ...state.editor.tools, activeTool: mode.previousTool, interactionMode: { type: "tool", tool: mode.previousTool }, }, }, }; }, }; export const toolCommands = [ toolSetActiveCommand, toolSetBrushSettingsCommand, toolSetBrushPreviewCommand, toolSetBrushStrokePreviewCommand, toolSetMaskViewModeCommand, toolEnterMaskEditCommand, toolExitMaskEditCommand, toolEnterTemporaryPanCommand, toolExitTemporaryPanCommand, ] satisfies Command[]; function clampNumber(value: number, min: number, max: number) { if (!Number.isFinite(value)) return min; return Math.max(min, Math.min(max, value)); } type LayerLocation = { artboardId: ArtboardId; layer: Layer; }; function findLayerLocation(document: ImageDocument, layerId: LayerId): LayerLocation | undefined { for (const artboard of document.artboards) { const layer = findLayerInTree(artboard.layers, layerId); if (layer) return { artboardId: artboard.id, layer }; } return undefined; } function findLayerInTree(layers: readonly Layer[], layerId: LayerId): Layer | undefined { for (const layer of layers) { if (layer.id === layerId) return layer; if (layer.type === "group") { const child = findLayerInTree(layer.children, layerId); if (child) return child; } } return undefined; }