- Enhanced cursor behavior for new tools: semantic select, mask lasso, and mask rectangle. - Updated mask edit state to include mask asset ID and kind. - Implemented inpaint region commands for adding, applying, and removing inpaint regions. - Introduced new operations for lasso and semantic selection tools. - Created UI components for candidate review and inpaint region management. - Added tests for inpaint region commands to ensure functionality. - Updated various components to support new inpaint features and improve user experience.
126 lines
5.2 KiB
TypeScript
126 lines
5.2 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import type { ImageDocument } from "@core/document";
|
|
import type { Layer } from "@core/layer";
|
|
import { createDocumentReadIndex, forEachLayerBackToFront, resolveIndexedLayerBounds } from "./document-indexes";
|
|
|
|
const document: ImageDocument = {
|
|
inpaintRegions: [],
|
|
id: "d1",
|
|
name: "Indexed Document",
|
|
version: 1,
|
|
assets: [
|
|
{ id: "asset-target", name: "Target", mimeType: "image/png", source: "asset://target", intrinsicSize: { w: 100, h: 50 } },
|
|
{ id: "asset-mask", name: "Mask", mimeType: "image/png", source: "asset://mask", intrinsicSize: { w: 100, h: 50 } },
|
|
{ id: "asset-nested", name: "Nested", mimeType: "image/png", source: "asset://nested", intrinsicSize: { w: 20, h: 10 } },
|
|
],
|
|
artboards: [
|
|
{
|
|
id: "a1",
|
|
name: "Artboard",
|
|
bounds: { x: 0, y: 0, w: 400, h: 300 },
|
|
backgroundColor: "transparent",
|
|
visible: true,
|
|
locked: false,
|
|
layers: [
|
|
raster("mask", "Mask", "asset-mask"),
|
|
{
|
|
...raster("target", "Target", "asset-target", { x: 10, y: 20 }, { x: 0.5, y: 0.5 }),
|
|
layerMask: { kind: "raster", maskLayerId: "mask", enabled: true, inverted: false },
|
|
},
|
|
group("group", "Group", [
|
|
raster("nested-mask", "Nested Mask", "asset-mask"),
|
|
{ ...raster("nested-target", "Nested Target", "asset-nested", { x: 80, y: 10 }, { x: 2, y: 3 }), clippingMask: { maskLayerId: "nested-mask" } },
|
|
]),
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
describe("document read indexes", () => {
|
|
test("indexes assets, layers, layer info, masks, and display counts", () => {
|
|
const index = createDocumentReadIndex(document);
|
|
|
|
expect(index.assetById.get("asset-target")).toBe(document.assets[0]);
|
|
expect(index.layerById.get("nested-target")?.name).toBe("Nested Target");
|
|
expect(index.layerInfoById.get("target")).toMatchObject({ artboardId: "a1", index: 1 });
|
|
expect(index.layerInfoById.get("nested-target")).toMatchObject({ artboardId: "a1", parentGroupId: "group", index: 1 });
|
|
expect(index.maskLayerIds).toEqual(new Set(["mask", "nested-mask"]));
|
|
expect(index.maskLayerIdsByArtboardId.get("a1")).toEqual(new Set(["mask", "nested-mask"]));
|
|
expect(index.maskLayerIdsByLayerList.get(document.artboards[0]!.layers)).toEqual(new Set(["mask", "nested-mask"]));
|
|
expect(index.maskLayerIdsByLayerList.get(groupLayer(document, "group").children)).toEqual(new Set(["nested-mask"]));
|
|
expect(index.displayLayerCountByArtboardId.get("a1")).toBe(3);
|
|
});
|
|
|
|
test("resolves layer bounds from indexed assets without scanning the document", () => {
|
|
const index = createDocumentReadIndex(document);
|
|
|
|
expect(resolveIndexedLayerBounds(index, "target")).toEqual({ x: 10, y: 20, w: 50, h: 25 });
|
|
expect(resolveIndexedLayerBounds(index, "group")).toEqual({ x: 0, y: 0, w: 120, h: 50 });
|
|
expect(resolveIndexedLayerBounds(index, raster("missing", "Missing", "missing-asset"))).toBeUndefined();
|
|
});
|
|
|
|
test("resolves a cropped layer from its retained source-pixel position", () => {
|
|
const cropped = { ...document, artboards: [{ ...document.artboards[0]!, layers: [{ ...raster("cropped", "Cropped", "asset-target", { x: 10, y: 20 }, { x: 2, y: 3 }), sourceRect: { x: 5, y: 4, w: 20, h: 10 } }] }] };
|
|
expect(resolveIndexedLayerBounds(createDocumentReadIndex(cropped), "cropped")).toEqual({ x: 20, y: 32, w: 40, h: 30 });
|
|
});
|
|
|
|
test("visits layers back to front without mutating source order", () => {
|
|
const layers = document.artboards[0]!.layers;
|
|
const visited: string[] = [];
|
|
|
|
forEachLayerBackToFront(layers, (layer) => visited.push(layer.id));
|
|
|
|
expect(visited).toEqual(["group", "target", "mask"]);
|
|
expect(layers.map((layer) => layer.id)).toEqual(["mask", "target", "group"]);
|
|
});
|
|
|
|
test("indexes pathological nesting without recursive stack overflow", () => {
|
|
let layers: Layer[] = [raster("leaf", "Leaf", "asset-nested", { x: 80, y: 10 }, { x: 2, y: 3 })];
|
|
for (let depth = 0; depth < 10_000; depth += 1) layers = [group(`group-${depth}`, `Group ${depth}`, layers)];
|
|
const deepDocument = { ...document, artboards: [{ ...document.artboards[0]!, layers }] };
|
|
|
|
const index = createDocumentReadIndex(deepDocument);
|
|
|
|
expect(index.layerById.size).toBe(10_001);
|
|
expect(resolveIndexedLayerBounds(index, layers[0]!)).toEqual({ x: 80, y: 10, w: 40, h: 30 });
|
|
});
|
|
});
|
|
|
|
function raster(
|
|
id: string,
|
|
name: string,
|
|
assetId: string,
|
|
position = { x: 0, y: 0 },
|
|
scale = { x: 1, y: 1 },
|
|
): Extract<Layer, { type: "raster" }> {
|
|
return {
|
|
id,
|
|
type: "raster",
|
|
name,
|
|
visible: true,
|
|
locked: false,
|
|
opacity: 1,
|
|
assetId,
|
|
transform: { position, scale, rotation: 0 },
|
|
};
|
|
}
|
|
|
|
function group(id: string, name: string, children: Layer[]): Extract<Layer, { type: "group" }> {
|
|
return {
|
|
id,
|
|
type: "group",
|
|
name,
|
|
visible: true,
|
|
locked: false,
|
|
opacity: 1,
|
|
transform: { position: { x: 0, y: 0 }, scale: { x: 1, y: 1 }, rotation: 0 },
|
|
children,
|
|
};
|
|
}
|
|
|
|
function groupLayer(document: ImageDocument, id: string): Extract<Layer, { type: "group" }> {
|
|
const layer = document.artboards[0]!.layers.find((candidate) => candidate.id === id);
|
|
if (!layer || layer.type !== "group") throw new Error(`Missing group ${id}`);
|
|
return layer;
|
|
}
|