feat: add contextual layer inspector

This commit is contained in:
syntaxbullet
2026-07-11 12:07:36 +02:00
parent bef2fb3051
commit 4d7358af4b
14 changed files with 405 additions and 42 deletions

View File

@@ -6,6 +6,7 @@ import type { ArtboardId, AssetId, LayerId } from "@core/id";
import type { ImageLayer } from "@core/image-layer";
import { getLayerMask } from "@core/layer-mask-utils";
import type { RasterLayer } from "@core/raster-layer";
import type { Layer } from "@core/layer";
import type { LayerGroup } from "@core/layer-group";
import type { Command } from "./command";
import { commandIds } from "./ids";
@@ -98,6 +99,16 @@ export type DocumentSetLayerLockedPayload = {
locked: boolean;
};
export type DocumentSetLayerOpacityPayload = {
layerId: LayerId;
opacity: number;
};
export type DocumentDuplicateLayerPayload = {
layerId: LayerId;
idByLayerId: Record<LayerId, LayerId>;
};
export type DocumentRenameLayerPayload = {
layerId: LayerId;
name: string;
@@ -402,6 +413,56 @@ export const documentSetLayerLockedCommand: Command<DocumentSetLayerLockedPayloa
},
};
export const documentSetLayerOpacityCommand: Command<DocumentSetLayerOpacityPayload> = {
id: commandIds.documentSetLayerOpacity,
name: "Set layer opacity",
execute({ state }, payload) {
const location = findLayerLocation(state.document, payload.layerId);
if (!location || location.layer.locked || !Number.isFinite(payload.opacity)) return state;
const opacity = Math.min(1, Math.max(0, payload.opacity));
return { ...state, document: mapLayerInDocument(state.document, payload.layerId, (layer) => ({ ...layer, opacity })) };
},
};
export const documentDuplicateLayerCommand: Command<DocumentDuplicateLayerPayload> = {
id: commandIds.documentDuplicateLayer,
name: "Duplicate layer",
execute({ state }, payload) {
const location = findLayerLocation(state.document, payload.layerId);
if (!location || location.layer.locked) return state;
const maskId = getLayerMask(location.layer)?.maskLayerId;
const mask = maskId ? location.siblings.find((layer) => layer.id === maskId) : undefined;
const sourceLayers = mask ? [mask, location.layer] : [location.layer];
const sourceIds = new Set(sourceLayers.flatMap((layer) => [...collectLayerIds(layer)]));
const mappedIds = [...sourceIds].map((id) => payload.idByLayerId[id]);
if (mappedIds.some((id) => !id) || new Set(mappedIds).size !== mappedIds.length || mappedIds.some((id) => findLayerLocation(state.document, id!))) return state;
const duplicates = sourceLayers.map((layer) => duplicateLayerTree(layer, payload.idByLayerId));
const insertionIndex = Math.max(...sourceLayers.map((layer) => location.siblings.findIndex((candidate) => candidate.id === layer.id))) + 1;
const siblings = [...location.siblings.slice(0, insertionIndex), ...duplicates, ...location.siblings.slice(insertionIndex)];
const duplicatedLayerId = payload.idByLayerId[payload.layerId];
if (!duplicatedLayerId) return state;
return {
...state,
document: replaceLayerListInDocument(state.document, location.artboardId, location.parentGroupId, siblings),
editor: { ...state.editor, selection: { artboardId: location.artboardId, layerIds: [duplicatedLayerId] } },
};
},
};
function duplicateLayerTree(layer: Layer, idByLayerId: Record<LayerId, LayerId>): Layer {
const layerMask = getLayerMask(layer);
const duplicatedMaskId = layerMask ? idByLayerId[layerMask.maskLayerId] : undefined;
const duplicated = {
...layer,
id: idByLayerId[layer.id]!,
name: `${layer.name} copy`,
transform: { ...layer.transform, position: { ...layer.transform.position }, scale: { ...layer.transform.scale } },
...(layerMask && duplicatedMaskId ? { layerMask: { ...layerMask, maskLayerId: duplicatedMaskId }, clippingMask: layer.clippingMask ? { maskLayerId: duplicatedMaskId } : undefined } : {}),
};
return layer.type === "group" ? { ...duplicated, type: "group", children: layer.children.map((child) => duplicateLayerTree(child, idByLayerId)) } : duplicated;
}
export const documentRenameLayerCommand: Command<DocumentRenameLayerPayload> = {
id: commandIds.documentRenameLayer,
name: "Rename layer",
@@ -619,6 +680,8 @@ export const documentCommands = [
documentRemoveLayerCommand,
documentSetLayerVisibleCommand,
documentSetLayerLockedCommand,
documentSetLayerOpacityCommand,
documentDuplicateLayerCommand,
documentRenameLayerCommand,
documentSetLayerClippingMaskCommand,
documentAddLayerMaskCommand,