- Implemented command palette with keyboard shortcut (⌘K) for opening. - Added commands for opening, closing, setting query, and selecting items in the command palette. - Created tests for command palette commands and input handling. - Enhanced layer actions with functions for adding, grouping, and deleting layers. - Updated UI components to integrate command palette and shortcuts.
160 lines
6.0 KiB
TypeScript
160 lines
6.0 KiB
TypeScript
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(`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"></svg>`)}`;
|
|
|
|
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(`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><rect width="${width}" height="${height}" fill="white"/></svg>`)}`;
|
|
|
|
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<string>();
|
|
|
|
function createGroup(name: string): Extract<Layer, { type: "group" }> {
|
|
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: [],
|
|
};
|
|
}
|