Files
image-studio/editor/document-indexes.ts
syntaxbullet 5eaf37ba28 feat: enhance layer mask handling and opacity management
- Introduced `getLayerMask` utility to streamline layer mask retrieval.
- Updated layer rendering logic to incorporate layer masks and opacity adjustments.
- Added functionality to prevent dropping a group into its descendants.
- Enhanced image texture rendering to support opacity parameters across various rendering functions.
- Implemented group layer bounds application for better scaling and positioning.
- Added tests to ensure correct behavior when handling layer masks and group layers.
- Created new types for asset generation provenance to track generated assets more effectively.
2026-07-05 16:06:23 +02:00

167 lines
5.3 KiB
TypeScript

import type { Asset } from "@core/asset";
import type { ImageDocument } from "@core/document";
import type { Rect } from "@core/geometry";
import type { ArtboardId, AssetId, LayerId } from "@core/id";
import type { Layer } from "@core/layer";
import { getLayerMask } from "@core/layer-mask-utils";
export type IndexedLayerInfo = {
artboardId: ArtboardId;
parentGroupId?: LayerId;
layer: Layer;
siblings: readonly Layer[];
index: number;
};
export type DocumentReadIndex = {
assetById: ReadonlyMap<AssetId, Asset>;
layerById: ReadonlyMap<LayerId, Layer>;
layerInfoById: ReadonlyMap<LayerId, IndexedLayerInfo>;
maskLayerIds: ReadonlySet<LayerId>;
maskLayerIdsByArtboardId: ReadonlyMap<ArtboardId, ReadonlySet<LayerId>>;
maskLayerIdsByLayerList: ReadonlyMap<readonly Layer[], ReadonlySet<LayerId>>;
displayLayerCountByArtboardId: ReadonlyMap<ArtboardId, number>;
};
export function createDocumentReadIndex(document: ImageDocument): DocumentReadIndex {
const assetById = new Map<AssetId, Asset>();
const layerById = new Map<LayerId, Layer>();
const layerInfoById = new Map<LayerId, IndexedLayerInfo>();
const maskLayerIds = new Set<LayerId>();
const maskLayerIdsByArtboardId = new Map<ArtboardId, ReadonlySet<LayerId>>();
const maskLayerIdsByLayerList = new Map<readonly Layer[], ReadonlySet<LayerId>>();
const displayLayerCountByArtboardId = new Map<ArtboardId, number>();
for (const asset of document.assets) assetById.set(asset.id, asset);
for (const artboard of document.artboards) {
const artboardMaskLayerIds = indexLayerTree({
layers: artboard.layers,
artboardId: artboard.id,
layerById,
layerInfoById,
documentMaskLayerIds: maskLayerIds,
maskLayerIdsByLayerList,
});
maskLayerIdsByArtboardId.set(artboard.id, artboardMaskLayerIds);
}
for (const artboard of document.artboards) {
displayLayerCountByArtboardId.set(artboard.id, countDisplayLayers(artboard.layers, maskLayerIds));
}
return {
assetById,
layerById,
layerInfoById,
maskLayerIds,
maskLayerIdsByArtboardId,
maskLayerIdsByLayerList,
displayLayerCountByArtboardId,
};
}
export function forEachLayerBackToFront(layers: readonly Layer[], visit: (layer: Layer) => void) {
for (let index = layers.length - 1; index >= 0; index -= 1) {
const layer = layers[index];
if (layer) visit(layer);
}
}
export function resolveIndexedLayerBounds(index: DocumentReadIndex, layerOrId: Layer | LayerId): Rect | undefined {
const layer = typeof layerOrId === "string" ? index.layerById.get(layerOrId) : layerOrId;
if (!layer) return undefined;
switch (layer.type) {
case "group":
return unionLayerBounds(index, layer.children);
case "image":
case "raster": {
const asset = index.assetById.get(layer.assetId);
if (!asset) return undefined;
return {
x: layer.transform.position.x,
y: layer.transform.position.y,
w: asset.intrinsicSize.w * layer.transform.scale.x,
h: asset.intrinsicSize.h * layer.transform.scale.y,
};
}
}
}
function indexLayerTree(options: {
layers: readonly Layer[];
artboardId: ArtboardId;
parentGroupId?: LayerId;
layerById: Map<LayerId, Layer>;
layerInfoById: Map<LayerId, IndexedLayerInfo>;
documentMaskLayerIds: Set<LayerId>;
maskLayerIdsByLayerList: Map<readonly Layer[], ReadonlySet<LayerId>>;
}): Set<LayerId> {
const layerListMaskLayerIds = new Set<LayerId>();
for (let index = 0; index < options.layers.length; index += 1) {
const layer = options.layers[index];
if (!layer) continue;
options.layerById.set(layer.id, layer);
options.layerInfoById.set(layer.id, {
artboardId: options.artboardId,
parentGroupId: options.parentGroupId,
layer,
siblings: options.layers,
index,
});
const layerMask = getLayerMask(layer);
if (layerMask) {
options.documentMaskLayerIds.add(layerMask.maskLayerId);
layerListMaskLayerIds.add(layerMask.maskLayerId);
}
if (layer.type === "group") {
const childMaskLayerIds = indexLayerTree({
...options,
layers: layer.children,
parentGroupId: layer.id,
});
for (const maskLayerId of childMaskLayerIds) layerListMaskLayerIds.add(maskLayerId);
}
}
options.maskLayerIdsByLayerList.set(options.layers, layerListMaskLayerIds);
return layerListMaskLayerIds;
}
function countDisplayLayers(layers: readonly Layer[], maskLayerIds: ReadonlySet<LayerId>): number {
let count = 0;
for (const layer of layers) {
if (maskLayerIds.has(layer.id)) continue;
count += 1;
if (layer.type === "group") count += countDisplayLayers(layer.children, maskLayerIds);
}
return count;
}
function unionLayerBounds(index: DocumentReadIndex, layers: readonly Layer[]): Rect | undefined {
let bounds: Rect | undefined;
for (const layer of layers) {
const layerBounds = resolveIndexedLayerBounds(index, layer);
if (!layerBounds) continue;
bounds = bounds ? unionRects(bounds, layerBounds) : layerBounds;
}
return bounds;
}
function unionRects(a: Rect, b: Rect): Rect {
const minX = Math.min(a.x, b.x);
const minY = Math.min(a.y, b.y);
const maxX = Math.max(a.x + a.w, b.x + b.w);
const maxY = Math.max(a.y + a.h, b.y + b.h);
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
}