Files
image-studio/view/canvas/renderFrame.test.ts
syntaxbullet 38565382c3 feat: add feather brush tool with adjustable settings and blending functionality
- Implemented feather brush tool in the brushRaster module, allowing for feathered edges in brush strokes.
- Added new FeatherControls component for UI adjustments of feather settings including size, radius, strength, and smoothing.
- Updated brush preview logic to accommodate feather tool alongside existing brush and eraser tools.
- Enhanced layer rendering to support feather mask previews and interactions.
- Introduced blending logic for feathered strokes to mix blurred mask values with original pixels.
- Added unit tests for feather blending functionality and tool keybindings.
- Updated cursor handling to reflect feather tool usage.
2026-07-11 22:08:25 +02:00

222 lines
5.9 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { createInitialAppState } from "@editor/initial-state";
import type { AppState } from "@editor/state";
import { canvasRenderFramesEqual, selectCanvasRenderFrame } from "./renderFrame";
describe("canvas render frame selection", () => {
test("ignores non-visual tool and history state", () => {
const state = createInitialAppState("Test");
const next: AppState = {
...state,
editor: {
...state.editor,
tools: {
...state.editor.tools,
generate: {
...state.editor.tools.generate,
prompt: "new prompt",
steps: state.editor.tools.generate.steps + 1,
},
chromaKey: {
...state.editor.tools.chromaKey,
tolerance: state.editor.tools.chromaKey.tolerance + 1,
},
magicWand: {
...state.editor.tools.magicWand,
contiguous: !state.editor.tools.magicWand.contiguous,
},
},
},
history: { past: [{ document: state.document, editor: state.editor }], future: [] },
};
expect(canvasRenderFramesEqual(selectCanvasRenderFrame(state), selectCanvasRenderFrame(next))).toBe(true);
});
test("detects document changes", () => {
const state = createInitialAppState("Test");
const next = {
...state,
document: {
...state.document,
name: "Changed",
},
};
expect(canvasRenderFramesEqual(selectCanvasRenderFrame(state), selectCanvasRenderFrame(next))).toBe(false);
});
test("compares stable visual editor fields structurally", () => {
const state = createInitialAppState("Test");
const sameSelectionValues = {
...state,
editor: {
...state.editor,
selection: {
artboardId: state.editor.selection.artboardId,
layerIds: [...state.editor.selection.layerIds],
},
},
};
const changedBrush = {
...state,
editor: {
...state.editor,
tools: {
...state.editor.tools,
brush: {
...state.editor.tools.brush,
size: state.editor.tools.brush.size + 1,
},
},
},
};
const changedPreview = {
...state,
editor: {
...state.editor,
brushPreview: { position: { x: 10, y: 20 } },
},
};
const changedFeather = {
...state,
editor: {
...state.editor,
tools: {
...state.editor.tools,
feather: { ...state.editor.tools.feather, size: state.editor.tools.feather.size + 1 },
},
},
};
expect(canvasRenderFramesEqual(selectCanvasRenderFrame(state), selectCanvasRenderFrame(sameSelectionValues))).toBe(true);
expectFrameChanged(state, changedBrush);
expectFrameChanged(state, changedFeather);
expectFrameChanged(state, changedPreview);
});
for (const [name, change] of visualEditorChanges) {
test(`detects ${name} changes`, () => {
const state = createInitialAppState("Test");
expectFrameChanged(state, change(state));
});
}
});
const visualEditorChanges: Array<[string, (state: AppState) => AppState]> = [
[
"viewport",
(state) => ({
...state,
editor: {
...state.editor,
viewport: {
...state.editor.viewport,
center: { x: state.editor.viewport.center.x + 1, y: state.editor.viewport.center.y },
},
},
}),
],
[
"selection",
(state) => ({
...state,
editor: {
...state.editor,
selection: { artboardId: "artboard", layerIds: ["layer"] },
},
}),
],
[
"transform session",
(state) => ({
...state,
editor: {
...state.editor,
transformSession: {
target: { type: "artboard", id: "artboard" },
handle: "body",
startPoint: { x: 1, y: 2 },
initialBounds: { x: 0, y: 0, w: 100, h: 80 },
},
},
}),
],
[
"mask edit",
(state) => ({
...state,
editor: {
...state.editor,
maskEdit: { kind: "layerMask", targetLayerId: "target", maskLayerId: "mask", maskAssetId: "mask-asset", viewMode: "overlay" },
},
}),
],
[
"brush stroke preview",
(state) => ({
...state,
editor: {
...state.editor,
brushStrokePreview: { layerId: "layer", assetId: "asset", source: "preview" },
},
}),
],
[
"generation candidate preview",
(state) => ({
...state,
editor: {
...state.editor,
generation: {
...state.editor.generation,
candidates: [
{
id: "candidate",
source: "data:image/png;base64,candidate",
mimeType: "image/png",
intrinsicSize: { w: 64, h: 64 },
mode: "text-to-image",
settings: state.editor.tools.generate,
seed: 12,
width: 64,
height: 64,
placement: {
artboardId: "artboard",
layerName: "Candidate",
transform: { position: { x: 1, y: 2 }, scale: { x: 1, y: 1 }, rotation: 0 },
},
},
],
selectedCandidateId: "candidate",
compareMode: "result",
},
},
}),
],
[
"active tool",
(state) => ({
...state,
editor: {
...state.editor,
tools: { ...state.editor.tools, activeTool: "brush" },
},
}),
],
[
"interaction mode",
(state) => ({
...state,
editor: {
...state.editor,
tools: { ...state.editor.tools, interactionMode: { type: "temporary-pan", previousTool: "select" } },
},
}),
],
];
function expectFrameChanged(previous: AppState, next: AppState) {
expect(canvasRenderFramesEqual(selectCanvasRenderFrame(previous), selectCanvasRenderFrame(next))).toBe(false);
}