perf(document): index layer read paths
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
import { useRef, useState, type DragEvent, type MutableRefObject } from "react";
|
||||
import { useMemo, useRef, useState, type DragEvent, type MutableRefObject } from "react";
|
||||
import { ArrowDown, ArrowUp, DownloadSimple, Eye, EyeSlash, FolderPlus, Lock, LockOpen, Plus, Stack, Trash } from "@phosphor-icons/react";
|
||||
import { commandIds } from "@commands/ids";
|
||||
import type { ImageDocument } from "@core/document";
|
||||
import type { Layer } from "@core/layer";
|
||||
import type { ArtboardId } from "@core/id";
|
||||
import { createDocumentReadIndex, resolveIndexedLayerBounds, type DocumentReadIndex, type IndexedLayerInfo } from "@editor/document-indexes";
|
||||
import type { MaskEditState, SelectionState } from "@editor/state";
|
||||
import type { AppStore } from "@editor/store";
|
||||
import { resolveTransformTargetBounds } from "@editor/transform-targets";
|
||||
import { findGroup, findLayerInfoInDocument, resolveLayerDrop, type LayerInfo } from "@input/index";
|
||||
import { resolveLayerDrop } from "@input/index";
|
||||
import { downloadArtboardPng } from "./exportArtboardPng";
|
||||
|
||||
export type LayersSheetProps = {
|
||||
@@ -19,7 +19,7 @@ export type LayersSheetProps = {
|
||||
};
|
||||
|
||||
export function LayersSheet({ document, selection, maskEdit, open, dispatch }: LayersSheetProps) {
|
||||
const draggedLayerId = useRef<string>();
|
||||
const draggedLayerId = useRef<string | undefined>(undefined);
|
||||
const [editingTitle, setEditingTitle] = useState<EditingTitle>();
|
||||
|
||||
return (
|
||||
@@ -57,11 +57,13 @@ function LayersSheetBody({
|
||||
editingTitle: EditingTitle | undefined;
|
||||
setEditingTitle: (editingTitle: EditingTitle | undefined) => void;
|
||||
}) {
|
||||
const documentIndex = useMemo(() => createDocumentReadIndex(document), [document]);
|
||||
const selectedArtboardId = selection.artboardId ?? document.artboards[0]?.id;
|
||||
const selectedLayer = findLayerInfoInDocument(document, selection.layerIds[0]);
|
||||
const selectedLayerId = selection.layerIds[0];
|
||||
const selectedLayer = selectedLayerId ? documentIndex.layerInfoById.get(selectedLayerId) : undefined;
|
||||
const canGroup = Boolean(selection.artboardId && selection.layerIds.length > 0);
|
||||
const canUngroup = selectedLayer?.layer.type === "group";
|
||||
const maskLayerIds = collectDocumentMaskLayerIds(document);
|
||||
const maskLayerIds = documentIndex.maskLayerIds;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -87,10 +89,10 @@ function LayersSheetBody({
|
||||
<button type="button" className={toolbarButtonClass()} aria-label="Ungroup" title="Ungroup" disabled={!canUngroup} onClick={() => selectedLayer && dispatch(commandIds.documentUngroupLayer, { groupId: selectedLayer.layer.id })}>
|
||||
<Stack size={24} weight="fill" />
|
||||
</button>
|
||||
<button type="button" className={toolbarButtonClass()} aria-label="Move layer up" title="Move layer up" disabled={!selectedLayer} onClick={() => selectedLayer && moveLayer(document, selectedLayer, -1, dispatch)}>
|
||||
<button type="button" className={toolbarButtonClass()} aria-label="Move layer up" title="Move layer up" disabled={!selectedLayer} onClick={() => selectedLayer && moveLayer(documentIndex, selectedLayer, -1, dispatch)}>
|
||||
<ArrowUp size={24} />
|
||||
</button>
|
||||
<button type="button" className={toolbarButtonClass()} aria-label="Move layer down" title="Move layer down" disabled={!selectedLayer} onClick={() => selectedLayer && moveLayer(document, selectedLayer, 1, dispatch)}>
|
||||
<button type="button" className={toolbarButtonClass()} aria-label="Move layer down" title="Move layer down" disabled={!selectedLayer} onClick={() => selectedLayer && moveLayer(documentIndex, selectedLayer, 1, dispatch)}>
|
||||
<ArrowDown size={24} />
|
||||
</button>
|
||||
<button type="button" className={toolbarButtonClass()} aria-label="Delete selection" title="Delete selection" disabled={!selectedLayer && !selection.artboardId} onClick={() => deleteSelection(selection, selectedLayer, dispatch)}>
|
||||
@@ -98,7 +100,10 @@ function LayersSheetBody({
|
||||
</button>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-auto pb-2">
|
||||
{document.artboards.map((artboard) => (
|
||||
{document.artboards.map((artboard) => {
|
||||
const displayLayerCount = documentIndex.displayLayerCountByArtboardId.get(artboard.id) ?? 0;
|
||||
|
||||
return (
|
||||
<section key={artboard.id} className="mb-5 last:mb-0">
|
||||
<div
|
||||
className={`flex h-12 w-full items-center gap-3 rounded-full px-4 text-left transition ${selection.artboardId === artboard.id && selection.layerIds.length === 0 ? "bg-white text-black" : "text-white/70 hover:bg-white/[0.06] hover:text-white"}`}
|
||||
@@ -149,16 +154,17 @@ function LayersSheetBody({
|
||||
>
|
||||
<DownloadSimple size={24} weight="regular" />
|
||||
</button>
|
||||
<span className={selection.artboardId === artboard.id && selection.layerIds.length === 0 ? "min-w-8 rounded-full bg-black/10 px-2 py-1 text-center text-xs text-black/45" : "min-w-8 rounded-full bg-white/10 px-2 py-1 text-center text-xs text-white/45"}>{countDisplayLayers(artboard.layers, maskLayerIds)}</span>
|
||||
<span className={selection.artboardId === artboard.id && selection.layerIds.length === 0 ? "min-w-8 rounded-full bg-black/10 px-2 py-1 text-center text-xs text-black/45" : "min-w-8 rounded-full bg-white/10 px-2 py-1 text-center text-xs text-white/45"}>{displayLayerCount}</span>
|
||||
</div>
|
||||
<div className="mt-2 space-y-2 pl-5">
|
||||
{countDisplayLayers(artboard.layers, maskLayerIds) === 0 ? (
|
||||
{displayLayerCount === 0 ? (
|
||||
<div className="rounded-[1.5rem] border border-dashed border-white/10 px-4 py-5 text-center text-white/35">No layers yet</div>
|
||||
) : (
|
||||
artboard.layers.map((layer) => (
|
||||
<LayerRow
|
||||
key={layer.id}
|
||||
document={document}
|
||||
documentIndex={documentIndex}
|
||||
artboardId={artboard.id}
|
||||
layer={layer}
|
||||
depth={0}
|
||||
@@ -174,7 +180,8 @@ function LayersSheetBody({
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -182,6 +189,7 @@ function LayersSheetBody({
|
||||
|
||||
function LayerRow({
|
||||
document,
|
||||
documentIndex,
|
||||
artboardId,
|
||||
layer,
|
||||
depth,
|
||||
@@ -194,6 +202,7 @@ function LayerRow({
|
||||
dispatch,
|
||||
}: {
|
||||
document: ImageDocument;
|
||||
documentIndex: DocumentReadIndex;
|
||||
artboardId: ArtboardId;
|
||||
layer: Layer;
|
||||
depth: number;
|
||||
@@ -208,8 +217,8 @@ function LayerRow({
|
||||
if (maskLayerIds.has(layer.id)) return null;
|
||||
|
||||
const selected = selectedLayerIds.includes(layer.id);
|
||||
const layerInfo = findLayerInfoInDocument(document, layer.id);
|
||||
const maskLayer = layer.clippingMask ? findLayerInfoInDocument(document, layer.clippingMask.maskLayerId)?.layer : undefined;
|
||||
const layerInfo = documentIndex.layerInfoById.get(layer.id);
|
||||
const maskLayer = layer.clippingMask ? documentIndex.layerById.get(layer.clippingMask.maskLayerId) : undefined;
|
||||
const canAddMask = Boolean(layerInfo && layer.type !== "group" && !layer.clippingMask);
|
||||
const editingMask = Boolean(maskEdit && layer.clippingMask && maskEdit.targetLayerId === layer.id && maskEdit.maskLayerId === layer.clippingMask.maskLayerId);
|
||||
const rowPadding = 12 + depth * 16;
|
||||
@@ -269,7 +278,7 @@ function LayerRow({
|
||||
<button
|
||||
type="button"
|
||||
className={editingMask || selected ? "rounded-full bg-black/10 px-3 py-1 text-xs text-black/65 transition hover:bg-black/15" : "rounded-full bg-white/5 px-3 py-1 text-xs text-white/45 transition hover:bg-sky-400/15 hover:text-sky-100"}
|
||||
onClick={() => addLayerMask(document, layerInfo, dispatch)}
|
||||
onClick={() => addLayerMask(documentIndex, layerInfo, dispatch)}
|
||||
>
|
||||
Add mask
|
||||
</button>
|
||||
@@ -309,6 +318,7 @@ function LayerRow({
|
||||
<LayerRow
|
||||
key={child.id}
|
||||
document={document}
|
||||
documentIndex={documentIndex}
|
||||
artboardId={artboardId}
|
||||
layer={child}
|
||||
depth={depth + 1}
|
||||
@@ -350,12 +360,12 @@ function RenameInput({ value, onChange, onCommit, onCancel }: { value: string; o
|
||||
);
|
||||
}
|
||||
|
||||
function addLayerMask(document: ImageDocument, layerInfo: LayerInfo, dispatch: AppStore["dispatch"]) {
|
||||
function addLayerMask(documentIndex: DocumentReadIndex, layerInfo: IndexedLayerInfo, dispatch: AppStore["dispatch"]) {
|
||||
const layer = layerInfo.layer;
|
||||
if (layer.type === "group") return;
|
||||
|
||||
const asset = document.assets.find((candidate) => candidate.id === layer.assetId);
|
||||
const bounds = resolveTransformTargetBounds(document, { type: "layer", id: layer.id });
|
||||
const asset = documentIndex.assetById.get(layer.assetId);
|
||||
const bounds = resolveIndexedLayerBounds(documentIndex, layer);
|
||||
if (!asset || !bounds) return;
|
||||
|
||||
const assetId = crypto.randomUUID();
|
||||
@@ -390,30 +400,6 @@ function addLayerMask(document: ImageDocument, layerInfo: LayerInfo, dispatch: A
|
||||
});
|
||||
}
|
||||
|
||||
function collectDocumentMaskLayerIds(document: ImageDocument): Set<string> {
|
||||
const ids = new Set<string>();
|
||||
for (const artboard of document.artboards) collectMaskLayerIds(artboard.layers, ids);
|
||||
return ids;
|
||||
}
|
||||
|
||||
function collectMaskLayerIds(layers: readonly Layer[], ids: Set<string>): Set<string> {
|
||||
for (const layer of layers) {
|
||||
if (layer.clippingMask) ids.add(layer.clippingMask.maskLayerId);
|
||||
if (layer.type === "group") collectMaskLayerIds(layer.children, ids);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function countDisplayLayers(layers: readonly Layer[], maskLayerIds: ReadonlySet<string>): 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 dropLayer(
|
||||
document: ImageDocument,
|
||||
sourceLayerId: string,
|
||||
@@ -431,7 +417,7 @@ function dropLayer(
|
||||
if (command) dispatch(commandIds.documentMoveLayer, command);
|
||||
}
|
||||
|
||||
function deleteSelection(selection: SelectionState, selectedLayer: LayerInfo | undefined, dispatch: AppStore["dispatch"]) {
|
||||
function deleteSelection(selection: SelectionState, selectedLayer: IndexedLayerInfo | undefined, dispatch: AppStore["dispatch"]) {
|
||||
if (selectedLayer) {
|
||||
dispatch(commandIds.documentRemoveLayer, { layerId: selectedLayer.layer.id });
|
||||
return;
|
||||
@@ -448,7 +434,7 @@ function addArtboard(document: ImageDocument, dispatch: AppStore["dispatch"]) {
|
||||
});
|
||||
}
|
||||
|
||||
function addLayer(document: ImageDocument, artboardId: ArtboardId, selectedLayer: LayerInfo | undefined, dispatch: AppStore["dispatch"]) {
|
||||
function addLayer(document: ImageDocument, artboardId: ArtboardId, selectedLayer: IndexedLayerInfo | undefined, dispatch: AppStore["dispatch"]) {
|
||||
const artboard = document.artboards.find((candidate) => candidate.id === artboardId);
|
||||
if (!artboard) return;
|
||||
|
||||
@@ -492,11 +478,9 @@ function groupSelection(artboardId: ArtboardId, layerIds: string[], dispatch: Ap
|
||||
dispatch(commandIds.documentGroupLayers, { artboardId, layerIds, group: createGroup("Group") });
|
||||
}
|
||||
|
||||
function moveLayer(document: ImageDocument, info: LayerInfo, direction: -1 | 1, dispatch: AppStore["dispatch"]) {
|
||||
const siblings = info.parentGroupId ? findGroup(document, info.parentGroupId)?.children : document.artboards.find((artboard) => artboard.id === info.artboardId)?.layers;
|
||||
if (!siblings) return;
|
||||
|
||||
const maskLayerIds = collectMaskLayerIds(siblings, new Set<string>());
|
||||
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 [];
|
||||
|
||||
@@ -522,7 +506,9 @@ function moveLayer(document: ImageDocument, info: LayerInfo, direction: -1 | 1,
|
||||
});
|
||||
}
|
||||
|
||||
function createGroup(name: string): Layer {
|
||||
const emptyLayerIds = new Set<string>();
|
||||
|
||||
function createGroup(name: string): Extract<Layer, { type: "group" }> {
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
type: "group",
|
||||
|
||||
Reference in New Issue
Block a user