import { commandIds } from "@commands/ids"; import type { ImageDocument } from "@core/document"; import type { ArtboardId } from "@core/id"; import type { Layer } from "@core/layer"; import { getLayerMask } from "@core/layer-mask-utils"; import type { DocumentReadIndex, IndexedLayerInfo } from "@editor/document-indexes"; import { resolveIndexedLayerBounds } from "@editor/document-indexes"; import type { SelectionState } from "@editor/state"; import type { AppStore } from "@editor/store"; export function addArtboard(document: ImageDocument, dispatch: AppStore["dispatch"]) { const index = document.artboards.length + 1; dispatch(commandIds.documentAddArtboard, { id: crypto.randomUUID(), name: `Artboard ${index}`, bounds: { x: (index - 1) * 40, y: (index - 1) * 40, w: 800, h: 600 }, }); } export function addEmptyLayer( document: ImageDocument, artboardId: ArtboardId, selectedLayer: IndexedLayerInfo | undefined, dispatch: AppStore["dispatch"], ) { const artboard = document.artboards.find((candidate) => candidate.id === artboardId); if (!artboard) return; const assetId = crypto.randomUUID(); const layerId = crypto.randomUUID(); const width = Math.max(1, Math.round(artboard.bounds.w)); const height = Math.max(1, Math.round(artboard.bounds.h)); const source = `data:image/svg+xml,${encodeURIComponent(``)}`; dispatch(commandIds.documentAddAsset, { asset: { id: assetId, name: "Empty Layer", mimeType: "image/svg+xml", source, intrinsicSize: { w: width, h: height }, }, }); dispatch(commandIds.documentAddRasterLayer, { artboardId, parentGroupId: selectedLayer?.layer.type === "group" ? selectedLayer.layer.id : undefined, layer: { id: layerId, type: "raster", name: "Layer", visible: true, locked: false, opacity: 1, assetId, transform: { position: { x: artboard.bounds.x, y: artboard.bounds.y }, scale: { x: 1, y: 1 }, rotation: 0 }, }, }); dispatch(commandIds.selectionSet, { artboardId, layerIds: [layerId] }); } export function addGroupLayer(artboardId: ArtboardId, dispatch: AppStore["dispatch"]) { dispatch(commandIds.documentAddGroupLayer, { artboardId, group: createGroup("Group") }); } export function groupLayers(artboardId: ArtboardId, layerIds: string[], dispatch: AppStore["dispatch"]) { dispatch(commandIds.documentGroupLayers, { artboardId, layerIds, group: createGroup("Group") }); } export function deleteSelection(selection: SelectionState, selectedLayer: IndexedLayerInfo | undefined, dispatch: AppStore["dispatch"]) { if (selectedLayer) { dispatch(commandIds.documentRemoveLayer, { layerId: selectedLayer.layer.id }); return; } if (selection.artboardId) dispatch(commandIds.documentRemoveArtboard, { id: selection.artboardId }); } export function moveLayer(documentIndex: DocumentReadIndex, info: IndexedLayerInfo, direction: -1 | 1, dispatch: AppStore["dispatch"]) { const siblings = info.siblings; const maskLayerIds = documentIndex.maskLayerIdsByLayerList.get(siblings) ?? emptyLayerIds; const blocks = siblings.flatMap((layer, index) => { if (maskLayerIds.has(layer.id)) return []; const layerMask = getLayerMask(layer); const maskIndex = layerMask ? siblings.findIndex((candidate) => candidate.id === layerMask.maskLayerId) : -1; const start = maskIndex >= 0 ? Math.min(maskIndex, index) : index; const end = maskIndex >= 0 ? Math.max(maskIndex, index) : index; return [{ layerId: layer.id, start, end, size: end - start + 1 }]; }); const currentBlockIndex = blocks.findIndex((block) => block.layerId === info.layer.id); const currentBlock = blocks[currentBlockIndex]; const targetBlock = blocks[currentBlockIndex + direction]; if (!currentBlock || !targetBlock) return; const insertionIndex = direction === -1 ? targetBlock.start : targetBlock.end + 1; const removedBeforeInsertion = currentBlock.end < insertionIndex ? currentBlock.size : currentBlock.start < insertionIndex ? insertionIndex - currentBlock.start : 0; dispatch(commandIds.documentMoveLayer, { layerId: info.layer.id, toArtboardId: info.artboardId, toParentGroupId: info.parentGroupId, toIndex: insertionIndex - removedBeforeInsertion, }); } export function addLayerMask(documentIndex: DocumentReadIndex, layerInfo: IndexedLayerInfo, dispatch: AppStore["dispatch"]) { const layer = layerInfo.layer; if (layer.type === "group") return; const asset = documentIndex.assetById.get(layer.assetId); const bounds = resolveIndexedLayerBounds(documentIndex, layer); if (!asset || !bounds) return; const assetId = crypto.randomUUID(); const maskLayerId = crypto.randomUUID(); const width = Math.max(1, Math.round(asset.intrinsicSize.w)); const height = Math.max(1, Math.round(asset.intrinsicSize.h)); const source = `data:image/svg+xml,${encodeURIComponent(``)}`; dispatch(commandIds.documentAddLayerMask, { layerId: layer.id, asset: { id: assetId, name: `${layer.name} Mask`, mimeType: "image/svg+xml", source, intrinsicSize: { w: width, h: height }, }, maskLayer: { id: maskLayerId, type: "raster", name: `${layer.name} Mask`, visible: true, locked: false, opacity: 1, assetId, transform: { position: { x: bounds.x, y: bounds.y }, scale: { x: bounds.w / width, y: bounds.h / height }, rotation: layer.transform.rotation, }, }, }); } const emptyLayerIds = new Set(); function createGroup(name: string): Extract { return { id: crypto.randomUUID(), type: "group", name, visible: true, locked: false, opacity: 1, transform: { position: { x: 0, y: 0 }, scale: { x: 1, y: 1 }, rotation: 0 }, children: [], }; }