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.
This commit is contained in:
syntaxbullet
2026-07-05 16:06:23 +02:00
parent a83024c35c
commit 5eaf37ba28
33 changed files with 635 additions and 88 deletions

View File

@@ -6,6 +6,7 @@ import {
generationApplyCandidateAsLayerCommand,
generationRemoveCandidateCommand,
generationReplaceCandidatePixelsCommand,
generationSetCompareModeCommand,
} from "./generation";
describe("generation commands", () => {
@@ -18,8 +19,20 @@ describe("generation commands", () => {
expect(added.editor.generation.candidates).toEqual([candidate]);
expect(added.editor.generation.selectedCandidateId).toBe(candidate.id);
expect(added.editor.generation.compareMode).toBe("result");
expect(removed.editor.generation.candidates).toEqual([]);
expect(removed.editor.generation.selectedCandidateId).toBeUndefined();
expect(removed.editor.generation.compareMode).toBe("result");
});
test("sets generation compare mode without touching candidates", () => {
const state = generationAddCandidateCommand.execute({ state: createInitialAppState("Test") }, { candidate: generationCandidate("candidate-1") });
const next = generationSetCompareModeCommand.execute({ state }, { mode: "split" });
expect(next.editor.generation.candidates).toEqual([generationCandidate("candidate-1")]);
expect(next.editor.generation.selectedCandidateId).toBe("candidate-1");
expect(next.editor.generation.compareMode).toBe("split");
});
test("applies candidates as top-level layers", () => {
@@ -31,9 +44,15 @@ describe("generation commands", () => {
);
expect(next.document.assets.find((asset) => asset.id === "generated-asset")?.source).toBe("generated-source");
expect(next.document.assets.find((asset) => asset.id === "generated-asset")?.provenance).toMatchObject({
kind: "generated",
candidateId: "candidate-1",
acceptance: "layer",
seed: 123,
});
expect(next.document.artboards[0]?.layers[0]?.id).toBe("generated-layer");
expect(next.editor.selection).toEqual({ artboardId: "a1", layerIds: ["generated-layer"] });
expect(next.editor.generation).toEqual({ candidates: [], selectedCandidateId: undefined });
expect(next.editor.generation).toEqual({ candidates: [], selectedCandidateId: undefined, compareMode: "result" });
});
test("replaces source asset pixels for inpaint candidates", () => {
@@ -46,8 +65,17 @@ describe("generation commands", () => {
expect(next.document.assets.find((asset) => asset.id === "source-asset")?.source).toBe("composited-source");
expect(next.document.assets.find((asset) => asset.id === "source-asset")?.mimeType).toBe("image/png");
expect(next.document.assets.find((asset) => asset.id === "source-asset")?.provenance).toMatchObject({
kind: "generated",
candidateId: "candidate-1",
acceptance: "replacement",
inpaint: {
sourceAssetId: "source-asset",
maskAssetId: "mask-asset",
},
});
expect(next.editor.selection).toEqual({ artboardId: "a1", layerIds: ["source-layer"] });
expect(next.editor.generation).toEqual({ candidates: [], selectedCandidateId: undefined });
expect(next.editor.generation).toEqual({ candidates: [], selectedCandidateId: undefined, compareMode: "result" });
});
});