feat(layers): add layer management panel
This commit is contained in:
@@ -1,6 +1,24 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createInitialAppState } from "@editor/initial-state";
|
||||
import { documentAddArtboardCommand, documentAddAssetCommand, documentAddImageLayerCommand, documentSetArtboardBoundsCommand } from "./document";
|
||||
import {
|
||||
documentAddArtboardCommand,
|
||||
documentAddAssetCommand,
|
||||
documentAddGroupLayerCommand,
|
||||
documentAddImageLayerCommand,
|
||||
documentGroupLayersCommand,
|
||||
documentMoveLayerCommand,
|
||||
documentRemoveArtboardCommand,
|
||||
documentRemoveLayerCommand,
|
||||
documentRenameArtboardCommand,
|
||||
documentRenameLayerCommand,
|
||||
documentSetArtboardBoundsCommand,
|
||||
documentSetArtboardLockedCommand,
|
||||
documentSetArtboardVisibleCommand,
|
||||
documentSetLayerClippingMaskCommand,
|
||||
documentSetLayerLockedCommand,
|
||||
documentSetLayerVisibleCommand,
|
||||
documentUngroupLayerCommand,
|
||||
} from "./document";
|
||||
|
||||
describe("document commands", () => {
|
||||
test("adds transparent artboard", () => {
|
||||
@@ -15,6 +33,8 @@ describe("document commands", () => {
|
||||
name: "Artboard 1",
|
||||
bounds: { x: 0, y: 0, w: 320, h: 240 },
|
||||
backgroundColor: "transparent",
|
||||
visible: true,
|
||||
locked: false,
|
||||
layers: [],
|
||||
},
|
||||
]);
|
||||
@@ -57,6 +77,114 @@ describe("document commands", () => {
|
||||
expect(next.document.artboards[0]?.layers.map((layer) => layer.id)).toEqual(["l1"]);
|
||||
});
|
||||
|
||||
test("adds group layers", () => {
|
||||
const state = documentAddArtboardCommand.execute(
|
||||
{ state: createInitialAppState("Test") },
|
||||
{ id: "a1", name: "Artboard 1", bounds: { x: 0, y: 0, w: 320, h: 240 } },
|
||||
);
|
||||
|
||||
const next = documentAddGroupLayerCommand.execute({ state }, { artboardId: "a1", group: group("g1", "Group") });
|
||||
|
||||
expect(next.document.artboards[0]?.layers).toEqual([group("g1", "Group")]);
|
||||
expect(next.editor.selection.layerIds).toEqual(["g1"]);
|
||||
});
|
||||
|
||||
test("moves layers", () => {
|
||||
const state = documentWithLayers([group("a", "A"), group("b", "B"), group("c", "C")]);
|
||||
|
||||
const next = documentMoveLayerCommand.execute({ state }, { layerId: "a", toArtboardId: "a1", toIndex: 2 });
|
||||
|
||||
expect(next.document.artboards[0]?.layers.map((layer) => layer.id)).toEqual(["b", "c", "a"]);
|
||||
});
|
||||
|
||||
test("groups and ungroups top-level layers", () => {
|
||||
const state = documentWithLayers([group("a", "A"), group("b", "B"), group("c", "C")]);
|
||||
const grouped = documentGroupLayersCommand.execute({ state }, { artboardId: "a1", layerIds: ["a", "b"], group: group("g", "Group") });
|
||||
|
||||
expect(grouped.document.artboards[0]?.layers.map((layer) => layer.id)).toEqual(["g", "c"]);
|
||||
expect((grouped.document.artboards[0]?.layers[0] as { children: { id: string }[] }).children.map((layer) => layer.id)).toEqual(["a", "b"]);
|
||||
expect(grouped.editor.selection.layerIds).toEqual(["g"]);
|
||||
|
||||
const ungrouped = documentUngroupLayerCommand.execute({ state: grouped }, { groupId: "g" });
|
||||
|
||||
expect(ungrouped.document.artboards[0]?.layers.map((layer) => layer.id)).toEqual(["a", "b", "c"]);
|
||||
expect(ungrouped.editor.selection.layerIds).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
test("removes layers", () => {
|
||||
const state = documentWithLayers([group("a", "A"), group("b", "B")]);
|
||||
const selectedState = { ...state, editor: { ...state.editor, selection: { artboardId: "a1", layerIds: ["a"] } } };
|
||||
const next = documentRemoveLayerCommand.execute({ state: selectedState }, { layerId: "a" });
|
||||
|
||||
expect(next.document.artboards[0]?.layers.map((layer) => layer.id)).toEqual(["b"]);
|
||||
expect(next.editor.selection.layerIds).toEqual([]);
|
||||
});
|
||||
|
||||
test("removes artboards and clears artboard selection", () => {
|
||||
const state = documentAddArtboardCommand.execute(
|
||||
{ state: createInitialAppState("Test") },
|
||||
{ id: "a1", name: "Artboard 1", bounds: { x: 0, y: 0, w: 320, h: 240 } },
|
||||
);
|
||||
const selectedState = { ...state, editor: { ...state.editor, selection: { artboardId: "a1", layerIds: [] } } };
|
||||
|
||||
const next = documentRemoveArtboardCommand.execute({ state: selectedState }, { id: "a1" });
|
||||
|
||||
expect(next.document.artboards).toEqual([]);
|
||||
expect(next.editor.selection).toEqual({ layerIds: [] });
|
||||
});
|
||||
|
||||
test("renames artboards and layers", () => {
|
||||
const state = documentWithLayers([group("a", "A")]);
|
||||
const renamedArtboard = documentRenameArtboardCommand.execute({ state }, { id: "a1", name: "New Artboard" });
|
||||
const renamedLayer = documentRenameLayerCommand.execute({ state: renamedArtboard }, { layerId: "a", name: "New Layer" });
|
||||
|
||||
expect(renamedLayer.document.artboards[0]?.name).toBe("New Artboard");
|
||||
expect(renamedLayer.document.artboards[0]?.layers[0]?.name).toBe("New Layer");
|
||||
});
|
||||
|
||||
test("sets and clears layer clipping masks", () => {
|
||||
const state = documentWithLayers([group("mask", "Mask"), group("target", "Target")]);
|
||||
const masked = documentSetLayerClippingMaskCommand.execute({ state }, { layerId: "target", maskLayerId: "mask" });
|
||||
|
||||
expect(masked.document.artboards[0]?.layers[1]?.clippingMask).toEqual({ maskLayerId: "mask" });
|
||||
|
||||
const cleared = documentSetLayerClippingMaskCommand.execute({ state: masked }, { layerId: "target" });
|
||||
|
||||
expect(cleared.document.artboards[0]?.layers[1]?.clippingMask).toBeUndefined();
|
||||
});
|
||||
|
||||
test("moves masked layers directly after their mask", () => {
|
||||
const state = documentWithLayers([group("target", "Target"), group("other", "Other"), group("mask", "Mask")]);
|
||||
|
||||
const masked = documentSetLayerClippingMaskCommand.execute({ state }, { layerId: "target", maskLayerId: "mask" });
|
||||
|
||||
expect(masked.document.artboards[0]?.layers.map((layer) => layer.id)).toEqual(["other", "mask", "target"]);
|
||||
expect(masked.document.artboards[0]?.layers[2]?.clippingMask).toEqual({ maskLayerId: "mask" });
|
||||
});
|
||||
|
||||
test("sets artboard visibility and lock state", () => {
|
||||
const state = documentAddArtboardCommand.execute(
|
||||
{ state: createInitialAppState("Test") },
|
||||
{ id: "a1", name: "Artboard 1", bounds: { x: 0, y: 0, w: 320, h: 240 } },
|
||||
);
|
||||
|
||||
const hidden = documentSetArtboardVisibleCommand.execute({ state }, { id: "a1", visible: false });
|
||||
const locked = documentSetArtboardLockedCommand.execute({ state: hidden }, { id: "a1", locked: true });
|
||||
|
||||
expect(locked.document.artboards[0]?.visible).toBe(false);
|
||||
expect(locked.document.artboards[0]?.locked).toBe(true);
|
||||
});
|
||||
|
||||
test("sets layer visibility and lock state", () => {
|
||||
const state = documentWithLayers([group("a", "A")]);
|
||||
|
||||
const hidden = documentSetLayerVisibleCommand.execute({ state }, { layerId: "a", visible: false });
|
||||
const locked = documentSetLayerLockedCommand.execute({ state: hidden }, { layerId: "a", locked: true });
|
||||
|
||||
expect(locked.document.artboards[0]?.layers[0]?.visible).toBe(false);
|
||||
expect(locked.document.artboards[0]?.layers[0]?.locked).toBe(true);
|
||||
});
|
||||
|
||||
test("sets artboard bounds", () => {
|
||||
const state = documentAddArtboardCommand.execute(
|
||||
{ state: createInitialAppState("Test") },
|
||||
@@ -68,3 +196,30 @@ describe("document commands", () => {
|
||||
expect(next.document.artboards[0]?.bounds).toEqual({ x: 10, y: 20, w: 640, h: 480 });
|
||||
});
|
||||
});
|
||||
|
||||
function documentWithLayers(layers: ReturnType<typeof group>[]) {
|
||||
return {
|
||||
...createInitialAppState("Test"),
|
||||
document: {
|
||||
...createInitialAppState("Test").document,
|
||||
artboards: [{ id: "a1", name: "Artboard 1", bounds: { x: 0, y: 0, w: 320, h: 240 }, backgroundColor: "transparent", visible: true, locked: false, layers }],
|
||||
},
|
||||
editor: {
|
||||
...createInitialAppState("Test").editor,
|
||||
selection: { artboardId: "a1", layerIds: [] },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function group(id: string, name: string) {
|
||||
return {
|
||||
id,
|
||||
type: "group" as const,
|
||||
name,
|
||||
visible: true,
|
||||
locked: false,
|
||||
opacity: 1,
|
||||
transform: { position: { x: 0, y: 0 }, scale: { x: 1, y: 1 }, rotation: 0 },
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user