feat: layer effects, bucket fill, simple brush

This commit is contained in:
2026-05-21 00:02:12 +07:00
parent bcf4650c70
commit e7dd9420e7
47 changed files with 1675 additions and 1038 deletions
+29 -14
View File
@@ -1,25 +1,28 @@
import { act, renderHook, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import type { Layer } from "@pien-studio/types";
import type { Layer, LayerEffect } from "@pien-studio/types";
import { useFaceBlurWorkflow } from "./use-face-blur-workflow";
function makeImageLayer(overrides: Partial<Layer> = {}): Layer {
return {
id: "layer-1",
type: "image",
type: "raster",
sourceUri: "data:image/png;base64,abc",
x: 0,
y: 0,
scale: 1,
rotation: 0,
opacity: 1,
effects: [],
visible: true,
...overrides,
};
}
describe("useFaceBlurWorkflow", () => {
it("selects all detected faces by default when no existing face blur", async () => {
const setImageLayerFaceBlur = vi.fn();
const setLayerEffect = vi.fn();
const removeLayerEffect = vi.fn();
const selectedLayer = makeImageLayer();
const faceDetections = [
{ x: 1, y: 2, width: 10, height: 12, label: "a", sourceWidth: 100, sourceHeight: 100 },
@@ -31,28 +34,37 @@ describe("useFaceBlurWorkflow", () => {
selectedLayer,
faceDetectionsLayerId: "layer-1",
faceDetections,
setImageLayerFaceBlur,
setLayerEffect,
removeLayerEffect,
}),
);
await waitFor(() => {
expect(result.current.selectedFaceIndices).toEqual([0, 1]);
expect(result.current.faceBlurPreview?.regions).toHaveLength(2);
const faceBlurEffect = result.current.faceBlurPreview?.effects.find((e) => e.kind === "face-blur");
expect(faceBlurEffect?.kind === "face-blur" ? faceBlurEffect.regions : undefined).toHaveLength(2);
});
});
it("keeps selection empty when selected image already has blur", async () => {
const setImageLayerFaceBlur = vi.fn();
const selectedLayer = makeImageLayer({
faceBlur: { method: "gaussian", amount: 14, regions: [{ x: 0, y: 0, width: 4, height: 4 }] },
});
const setLayerEffect = vi.fn();
const removeLayerEffect = vi.fn();
const faceBlurEffect: LayerEffect = {
kind: "face-blur",
enabled: true,
method: "gaussian",
amount: 14,
regions: [{ x: 0, y: 0, width: 4, height: 4 }],
};
const selectedLayer = makeImageLayer({ effects: [faceBlurEffect] });
const { result } = renderHook(() =>
useFaceBlurWorkflow({
selectedLayer,
faceDetectionsLayerId: "layer-1",
faceDetections: [{ x: 1, y: 2, width: 10, height: 12, label: "a", sourceWidth: 100, sourceHeight: 100 }],
setImageLayerFaceBlur,
setLayerEffect,
removeLayerEffect,
}),
);
@@ -63,7 +75,8 @@ describe("useFaceBlurWorkflow", () => {
});
it("toggles face selection and applies blur to selected regions", async () => {
const setImageLayerFaceBlur = vi.fn();
const setLayerEffect = vi.fn();
const removeLayerEffect = vi.fn();
const selectedLayer = makeImageLayer();
const faceDetections = [
{ x: 1, y: 2, width: 10, height: 12, label: "a", sourceWidth: 100, sourceHeight: 100 },
@@ -75,7 +88,8 @@ describe("useFaceBlurWorkflow", () => {
selectedLayer,
faceDetectionsLayerId: "layer-1",
faceDetections,
setImageLayerFaceBlur,
setLayerEffect,
removeLayerEffect,
}),
);
@@ -93,10 +107,11 @@ describe("useFaceBlurWorkflow", () => {
result.current.blurFaces(result.current.selectedFaceIndices);
});
expect(setImageLayerFaceBlur).toHaveBeenCalledTimes(1);
expect(setImageLayerFaceBlur).toHaveBeenCalledWith(
expect(setLayerEffect).toHaveBeenCalledTimes(1);
expect(setLayerEffect).toHaveBeenCalledWith(
"layer-1",
expect.objectContaining({
kind: "face-blur",
method: "gaussian",
amount: 14,
regions: expect.arrayContaining([