mirror of
https://github.com/YuzuZensai/Pien-Studio.git
synced 2026-09-02 14:18:35 +00:00
✨ feat: layer effects, bucket fill, simple brush
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import type { FaceBlurEffect } from "@pien-studio/types";
|
||||
import type { EffectDefinition } from "./registry";
|
||||
|
||||
function normalizeFaceBlur(effect: FaceBlurEffect): FaceBlurEffect {
|
||||
return {
|
||||
...effect,
|
||||
amount: Math.max(4, Math.min(40, Math.round(effect.amount))),
|
||||
regions: effect.regions.map((region) => ({
|
||||
x: Number.isFinite(region.x) ? region.x : 0,
|
||||
y: Number.isFinite(region.y) ? region.y : 0,
|
||||
width: Math.max(1, Number.isFinite(region.width) ? region.width : 1),
|
||||
height: Math.max(1, Number.isFinite(region.height) ? region.height : 1),
|
||||
sourceWidth: Number.isFinite(region.sourceWidth ?? NaN) ? region.sourceWidth : undefined,
|
||||
sourceHeight: Number.isFinite(region.sourceHeight ?? NaN) ? region.sourceHeight : undefined,
|
||||
censorColor: region.censorColor,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export const faceBlurDefinition: EffectDefinition<FaceBlurEffect> = {
|
||||
kind: "face-blur",
|
||||
normalize: normalizeFaceBlur,
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { LayerEffect } from "@pien-studio/types";
|
||||
import { faceBlurDefinition } from "./face-blur";
|
||||
|
||||
export type EffectDefinition<T extends LayerEffect = LayerEffect> = {
|
||||
kind: T["kind"];
|
||||
normalize: (effect: T) => T;
|
||||
};
|
||||
|
||||
const definitions = [faceBlurDefinition] as EffectDefinition[];
|
||||
|
||||
const effectRegistry = new Map<string, EffectDefinition>(
|
||||
definitions.map((def) => [def.kind, def]),
|
||||
);
|
||||
|
||||
export function getEffectDefinition(kind: string): EffectDefinition | undefined {
|
||||
return effectRegistry.get(kind);
|
||||
}
|
||||
|
||||
export function normalizeEffect(effect: LayerEffect): LayerEffect {
|
||||
const def = effectRegistry.get(effect.kind);
|
||||
if (!def) return effect;
|
||||
return def.normalize(effect as never);
|
||||
}
|
||||
@@ -19,12 +19,14 @@ describe("editor-core", () => {
|
||||
const project = createProject("new");
|
||||
const updated = addLayer(project, {
|
||||
id: "l1",
|
||||
type: "image",
|
||||
type: "raster",
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
effects: [],
|
||||
visible: true,
|
||||
});
|
||||
|
||||
expect(updated.layers).toHaveLength(1);
|
||||
@@ -41,6 +43,8 @@ describe("editor-core", () => {
|
||||
scale: 1,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
effects: [],
|
||||
visible: true,
|
||||
});
|
||||
|
||||
const moved = moveLayer(project, "l1", { dx: 15, dy: -5 });
|
||||
@@ -57,6 +61,8 @@ describe("editor-core", () => {
|
||||
scale: 1,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
effects: [],
|
||||
visible: true,
|
||||
});
|
||||
|
||||
const updated = updateLayerTransform(project, "l1", { scale: 1.35, rotation: 22 });
|
||||
@@ -74,6 +80,8 @@ describe("editor-core", () => {
|
||||
scale: 1,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
effects: [],
|
||||
visible: true,
|
||||
});
|
||||
const withSecond = addLayer(withFirst, {
|
||||
id: "l2",
|
||||
@@ -83,6 +91,8 @@ describe("editor-core", () => {
|
||||
scale: 1,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
effects: [],
|
||||
visible: true,
|
||||
});
|
||||
|
||||
const reordered = reorderLayer(withSecond, "l1", 1);
|
||||
|
||||
@@ -1,212 +1,36 @@
|
||||
import {
|
||||
PRESET_CANVAS_SIZES,
|
||||
ProjectFileSchema,
|
||||
type AspectRatio,
|
||||
type FaceBlurSettings,
|
||||
type Layer,
|
||||
type Project,
|
||||
type ProjectFile,
|
||||
} from "@pien-studio/types";
|
||||
export { createProject, setCanvasSize } from "./project";
|
||||
export { createLayer, addLayer, removeLayer, moveLayer, updateLayerTransform, reorderLayer, setLayerEffect, removeLayerEffect, setLayerVisible, setEffectEnabled } from "./layers";
|
||||
export type { LayerFactoryOptions, TransformPatch } from "./layers";
|
||||
export { normalizeProject } from "./normalize";
|
||||
export { serializeProjectFile, parseProjectFile } from "./serialization";
|
||||
export { getEffectDefinition, normalizeEffect } from "./effects/registry";
|
||||
export type { EffectDefinition } from "./effects/registry";
|
||||
export { faceBlurDefinition } from "./effects/face-blur";
|
||||
export { getToolDefinition, getAllTools } from "./tools/registry";
|
||||
export type { ToolDefinition, ToolInteractionMode, EditorToolId } from "./tools/registry";
|
||||
|
||||
const DEFAULT_ASPECT: AspectRatio = "4:5";
|
||||
|
||||
function defaultCanvas(aspect: AspectRatio) {
|
||||
const preset = PRESET_CANVAS_SIZES.find((p) => p.value === aspect) ?? PRESET_CANVAS_SIZES[1];
|
||||
return { width: preset.width, height: preset.height, unit: "px" as const };
|
||||
}
|
||||
|
||||
export function createProject(title: string, aspect: AspectRatio = DEFAULT_ASPECT): Project {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
title,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
canvas: defaultCanvas(aspect),
|
||||
aspectRatio: aspect,
|
||||
layers: [],
|
||||
};
|
||||
}
|
||||
|
||||
function withUpdatedTimestamp(project: Project, layers: Layer[]): Project {
|
||||
return { ...project, layers, updatedAt: new Date().toISOString() };
|
||||
}
|
||||
|
||||
type Delta = { dx: number; dy: number };
|
||||
export type TransformPatch = {
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
scale?: number;
|
||||
rotation?: number;
|
||||
opacity?: number;
|
||||
sourceUri?: string;
|
||||
faceBlur?: FaceBlurSettings;
|
||||
};
|
||||
|
||||
export type LayerFactoryOptions = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
sourceUri?: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
import type { Layer, LayerEffect, Project } from "@pien-studio/types";
|
||||
import { addLayer, moveLayer, removeLayer, reorderLayer, setLayerEffect, updateLayerTransform } from "./layers";
|
||||
import { setCanvasSize } from "./project";
|
||||
|
||||
export type EditorOperation =
|
||||
| { type: "addLayer"; layer: Layer }
|
||||
| { type: "removeLayer"; layerId: string }
|
||||
| { type: "moveLayer"; layerId: string; delta: Delta }
|
||||
| { type: "updateLayerTransform"; layerId: string; patch: TransformPatch }
|
||||
| { type: "moveLayer"; layerId: string; delta: { dx: number; dy: number } }
|
||||
| { type: "updateLayerTransform"; layerId: string; patch: import("./layers").TransformPatch }
|
||||
| { type: "reorderLayer"; layerId: string; toIndex: number }
|
||||
| { type: "setLayerEffect"; layerId: string; effect: LayerEffect }
|
||||
| { type: "setCanvasSize"; width: number; height: number; unit?: "px" | "in" | "cm" };
|
||||
|
||||
export function normalizeProject(project: Project): Project {
|
||||
const normalizedLayers = project.layers.map((layer) => ({
|
||||
...layer,
|
||||
width: layer.width !== undefined ? Math.max(1, Math.round(layer.width)) : undefined,
|
||||
height: layer.height !== undefined ? Math.max(1, Math.round(layer.height)) : undefined,
|
||||
scale: Number.isFinite(layer.scale) ? layer.scale : 1,
|
||||
rotation: Number.isFinite(layer.rotation) ? layer.rotation : 0,
|
||||
opacity: Number.isFinite(layer.opacity) ? Math.max(0, Math.min(1, layer.opacity)) : 1,
|
||||
faceBlur:
|
||||
layer.faceBlur && Array.isArray(layer.faceBlur.regions)
|
||||
? {
|
||||
method: layer.faceBlur.method,
|
||||
amount: Math.max(4, Math.min(40, Math.round(layer.faceBlur.amount))),
|
||||
regions: layer.faceBlur.regions.map((region) => ({
|
||||
x: Number.isFinite(region.x) ? region.x : 0,
|
||||
y: Number.isFinite(region.y) ? region.y : 0,
|
||||
width: Math.max(1, Number.isFinite(region.width) ? region.width : 1),
|
||||
height: Math.max(1, Number.isFinite(region.height) ? region.height : 1),
|
||||
sourceWidth: Number.isFinite(region.sourceWidth) ? region.sourceWidth : undefined,
|
||||
sourceHeight: Number.isFinite(region.sourceHeight) ? region.sourceHeight : undefined,
|
||||
censorColor: region.censorColor,
|
||||
})),
|
||||
censorColor: layer.faceBlur.censorColor,
|
||||
}
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
return {
|
||||
...project,
|
||||
canvas: {
|
||||
width: Math.max(1, Math.round(project.canvas.width)),
|
||||
height: Math.max(1, Math.round(project.canvas.height)),
|
||||
unit: project.canvas.unit,
|
||||
},
|
||||
layers: normalizedLayers,
|
||||
};
|
||||
}
|
||||
|
||||
export function addLayer(project: Project, layer: Layer): Project {
|
||||
return withUpdatedTimestamp(project, [...project.layers, layer]);
|
||||
}
|
||||
|
||||
export function createLayer(type: Layer["type"], options: LayerFactoryOptions = {}): Layer {
|
||||
return {
|
||||
id: options.id ?? crypto.randomUUID(),
|
||||
type,
|
||||
name: options.name,
|
||||
sourceUri: options.sourceUri,
|
||||
x: options.x ?? 110,
|
||||
y: options.y ?? 90,
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
scale: 1,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function removeLayer(project: Project, layerId: string): Project {
|
||||
const layers = project.layers.filter((layer) => layer.id !== layerId);
|
||||
return withUpdatedTimestamp(project, layers);
|
||||
}
|
||||
|
||||
export function moveLayer(project: Project, layerId: string, delta: Delta): Project {
|
||||
const layers = project.layers.map((layer) => {
|
||||
if (layer.id !== layerId) return layer;
|
||||
return { ...layer, x: layer.x + delta.dx, y: layer.y + delta.dy };
|
||||
});
|
||||
return withUpdatedTimestamp(project, layers);
|
||||
}
|
||||
|
||||
export function updateLayerTransform(project: Project, layerId: string, patch: TransformPatch): Project {
|
||||
const layers = project.layers.map((layer) => (layer.id === layerId ? { ...layer, ...patch } : layer));
|
||||
return withUpdatedTimestamp(project, layers);
|
||||
}
|
||||
|
||||
export function reorderLayer(project: Project, layerId: string, toIndex: number): Project {
|
||||
const fromIndex = project.layers.findIndex((l) => l.id === layerId);
|
||||
if (fromIndex < 0) return project;
|
||||
const layers = [...project.layers];
|
||||
const [picked] = layers.splice(fromIndex, 1);
|
||||
if (!picked) return project;
|
||||
const bounded = Math.max(0, Math.min(toIndex, layers.length));
|
||||
layers.splice(bounded, 0, picked);
|
||||
return withUpdatedTimestamp(project, layers);
|
||||
}
|
||||
|
||||
export function setCanvasSize(project: Project, width: number, height: number, unit: "px" | "in" | "cm" = "px"): Project {
|
||||
return {
|
||||
...project,
|
||||
canvas: {
|
||||
width: Math.max(1, Math.round(width)),
|
||||
height: Math.max(1, Math.round(height)),
|
||||
unit,
|
||||
},
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export function applyOperation(project: Project, operation: EditorOperation): Project {
|
||||
switch (operation.type) {
|
||||
case "addLayer":
|
||||
return addLayer(project, operation.layer);
|
||||
case "removeLayer":
|
||||
return removeLayer(project, operation.layerId);
|
||||
case "moveLayer":
|
||||
return moveLayer(project, operation.layerId, operation.delta);
|
||||
case "updateLayerTransform":
|
||||
return updateLayerTransform(project, operation.layerId, operation.patch);
|
||||
case "reorderLayer":
|
||||
return reorderLayer(project, operation.layerId, operation.toIndex);
|
||||
case "setCanvasSize":
|
||||
return setCanvasSize(project, operation.width, operation.height, operation.unit);
|
||||
default:
|
||||
return project;
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeProjectFile(project: Project, options?: { checkpointCount?: number }): string {
|
||||
const document: ProjectFile = {
|
||||
format: "pien.project",
|
||||
version: 1,
|
||||
exportedAt: new Date().toISOString(),
|
||||
app: { name: "pien.studio", platform: "web" },
|
||||
project,
|
||||
assets: [],
|
||||
history: {
|
||||
checkpointCount: options?.checkpointCount ?? 0,
|
||||
},
|
||||
};
|
||||
|
||||
return JSON.stringify(document, null, 2);
|
||||
}
|
||||
|
||||
export function parseProjectFile(raw: string): { ok: true; project: Project } | { ok: false; error: string } {
|
||||
try {
|
||||
const data = JSON.parse(raw) as unknown;
|
||||
const parsedEnvelope = ProjectFileSchema.safeParse(data);
|
||||
if (parsedEnvelope.success) {
|
||||
return { ok: true, project: normalizeProject(parsedEnvelope.data.project) };
|
||||
}
|
||||
|
||||
return { ok: false, error: "Invalid project format" };
|
||||
} catch {
|
||||
return { ok: false, error: "Invalid JSON" };
|
||||
case "addLayer": return addLayer(project, operation.layer);
|
||||
case "removeLayer": return removeLayer(project, operation.layerId);
|
||||
case "moveLayer": return moveLayer(project, operation.layerId, operation.delta);
|
||||
case "updateLayerTransform": return updateLayerTransform(project, operation.layerId, operation.patch);
|
||||
case "reorderLayer": return reorderLayer(project, operation.layerId, operation.toIndex);
|
||||
case "setLayerEffect": return setLayerEffect(project, operation.layerId, operation.effect);
|
||||
case "setCanvasSize": return setCanvasSize(project, operation.width, operation.height, operation.unit);
|
||||
default: return project;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import type { Layer, LayerEffect, Project } from "@pien-studio/types";
|
||||
|
||||
export type LayerFactoryOptions = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
sourceUri?: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
export type TransformPatch = {
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
scale?: number;
|
||||
rotation?: number;
|
||||
opacity?: number;
|
||||
sourceUri?: string;
|
||||
effects?: LayerEffect[];
|
||||
};
|
||||
|
||||
function withUpdatedAt(project: Project, layers: Layer[]): Project {
|
||||
return { ...project, layers, updatedAt: new Date().toISOString() };
|
||||
}
|
||||
|
||||
export function createLayer(type: Layer["type"], options: LayerFactoryOptions = {}): Layer {
|
||||
return {
|
||||
id: options.id ?? crypto.randomUUID(),
|
||||
type,
|
||||
name: options.name,
|
||||
sourceUri: options.sourceUri,
|
||||
effects: [],
|
||||
visible: true,
|
||||
x: options.x ?? 110,
|
||||
y: options.y ?? 90,
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
scale: 1,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function addLayer(project: Project, layer: Layer): Project {
|
||||
return withUpdatedAt(project, [...project.layers, layer]);
|
||||
}
|
||||
|
||||
export function removeLayer(project: Project, layerId: string): Project {
|
||||
return withUpdatedAt(project, project.layers.filter((l) => l.id !== layerId));
|
||||
}
|
||||
|
||||
export function moveLayer(project: Project, layerId: string, delta: { dx: number; dy: number }): Project {
|
||||
return withUpdatedAt(
|
||||
project,
|
||||
project.layers.map((l) => (l.id === layerId ? { ...l, x: l.x + delta.dx, y: l.y + delta.dy } : l)),
|
||||
);
|
||||
}
|
||||
|
||||
export function updateLayerTransform(project: Project, layerId: string, patch: TransformPatch): Project {
|
||||
return withUpdatedAt(
|
||||
project,
|
||||
project.layers.map((l) => (l.id === layerId ? { ...l, ...patch } : l)),
|
||||
);
|
||||
}
|
||||
|
||||
export function reorderLayer(project: Project, layerId: string, toIndex: number): Project {
|
||||
const fromIndex = project.layers.findIndex((l) => l.id === layerId);
|
||||
if (fromIndex < 0) return project;
|
||||
const layers = [...project.layers];
|
||||
const [picked] = layers.splice(fromIndex, 1);
|
||||
if (!picked) return project;
|
||||
layers.splice(Math.max(0, Math.min(toIndex, layers.length)), 0, picked);
|
||||
return withUpdatedAt(project, layers);
|
||||
}
|
||||
|
||||
export function setLayerEffect(project: Project, layerId: string, effect: LayerEffect): Project {
|
||||
return withUpdatedAt(
|
||||
project,
|
||||
project.layers.map((l) => {
|
||||
if (l.id !== layerId) return l;
|
||||
const idx = l.effects.findIndex((e) => e.kind === effect.kind);
|
||||
const effects = idx >= 0 ? l.effects.map((e, i) => (i === idx ? effect : e)) : [...l.effects, effect];
|
||||
return { ...l, effects };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function removeLayerEffect(project: Project, layerId: string, kind: LayerEffect["kind"]): Project {
|
||||
return withUpdatedAt(
|
||||
project,
|
||||
project.layers.map((l) => (l.id === layerId ? { ...l, effects: l.effects.filter((e) => e.kind !== kind) } : l)),
|
||||
);
|
||||
}
|
||||
|
||||
export function setLayerVisible(project: Project, layerId: string, visible: boolean): Project {
|
||||
return withUpdatedAt(
|
||||
project,
|
||||
project.layers.map((l) => (l.id === layerId ? { ...l, visible } : l)),
|
||||
);
|
||||
}
|
||||
|
||||
export function setEffectEnabled(project: Project, layerId: string, kind: LayerEffect["kind"], enabled: boolean): Project {
|
||||
return withUpdatedAt(
|
||||
project,
|
||||
project.layers.map((l) =>
|
||||
l.id === layerId
|
||||
? { ...l, effects: l.effects.map((e) => (e.kind === kind ? { ...e, enabled } : e)) }
|
||||
: l,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { Project } from "@pien-studio/types";
|
||||
import { normalizeEffect } from "./effects/registry";
|
||||
|
||||
export function normalizeProject(project: Project): Project {
|
||||
return {
|
||||
...project,
|
||||
canvas: {
|
||||
width: Math.max(1, Math.round(project.canvas.width)),
|
||||
height: Math.max(1, Math.round(project.canvas.height)),
|
||||
unit: project.canvas.unit,
|
||||
},
|
||||
layers: project.layers.map((layer) => ({
|
||||
...layer,
|
||||
width: layer.width !== undefined ? Math.max(1, Math.round(layer.width)) : undefined,
|
||||
height: layer.height !== undefined ? Math.max(1, Math.round(layer.height)) : undefined,
|
||||
scale: Number.isFinite(layer.scale) ? layer.scale : 1,
|
||||
rotation: Number.isFinite(layer.rotation) ? layer.rotation : 0,
|
||||
opacity: Number.isFinite(layer.opacity) ? Math.max(0, Math.min(1, layer.opacity)) : 1,
|
||||
effects: Array.isArray(layer.effects) ? layer.effects.map(normalizeEffect) : [],
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { PRESET_CANVAS_SIZES, type AspectRatio, type Project } from "@pien-studio/types";
|
||||
|
||||
const DEFAULT_ASPECT: AspectRatio = "4:5";
|
||||
|
||||
function defaultCanvas(aspect: AspectRatio) {
|
||||
const preset = PRESET_CANVAS_SIZES.find((p) => p.value === aspect) ?? PRESET_CANVAS_SIZES[1];
|
||||
return { width: preset.width, height: preset.height, unit: "px" as const };
|
||||
}
|
||||
|
||||
export function createProject(title: string, aspect: AspectRatio = DEFAULT_ASPECT): Project {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
title,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
canvas: defaultCanvas(aspect),
|
||||
aspectRatio: aspect,
|
||||
layers: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function setCanvasSize(
|
||||
project: Project,
|
||||
width: number,
|
||||
height: number,
|
||||
unit: "px" | "in" | "cm" = "px",
|
||||
): Project {
|
||||
return {
|
||||
...project,
|
||||
canvas: {
|
||||
width: Math.max(1, Math.round(width)),
|
||||
height: Math.max(1, Math.round(height)),
|
||||
unit,
|
||||
},
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ProjectFileSchema, type Project, type ProjectFile } from "@pien-studio/types";
|
||||
import { normalizeProject } from "./normalize";
|
||||
|
||||
export function serializeProjectFile(project: Project, options?: { checkpointCount?: number }): string {
|
||||
const document: ProjectFile = {
|
||||
format: "pien.project",
|
||||
version: 1,
|
||||
exportedAt: new Date().toISOString(),
|
||||
app: { name: "pien.studio", platform: "web" },
|
||||
project,
|
||||
assets: [],
|
||||
history: { checkpointCount: options?.checkpointCount ?? 0 },
|
||||
};
|
||||
return JSON.stringify(document, null, 2);
|
||||
}
|
||||
|
||||
export function parseProjectFile(raw: string): { ok: true; project: Project } | { ok: false; error: string } {
|
||||
try {
|
||||
const data = JSON.parse(raw) as unknown;
|
||||
const result = ProjectFileSchema.safeParse(data);
|
||||
if (result.success) return { ok: true, project: normalizeProject(result.data.project) };
|
||||
return { ok: false, error: "Invalid project format" };
|
||||
} catch {
|
||||
return { ok: false, error: "Invalid JSON" };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
export type ToolInteractionMode =
|
||||
| "select" // can select and drag layers
|
||||
| "pan" // pans the viewport, no layer interaction
|
||||
| "paint" // pixel-level tool, fires onLayerClick with canvas coords
|
||||
| "annotate"; // select-only, no drag (eg. face-blur region picking)
|
||||
|
||||
export type ToolDefinition = {
|
||||
id: string;
|
||||
interactionMode: ToolInteractionMode;
|
||||
allowsLayerDrag: boolean;
|
||||
allowsLayerResize: boolean;
|
||||
allowsLayerRotate: boolean;
|
||||
};
|
||||
|
||||
const definitions: ToolDefinition[] = [
|
||||
{
|
||||
id: "pointer",
|
||||
interactionMode: "select",
|
||||
allowsLayerDrag: true,
|
||||
allowsLayerResize: true,
|
||||
allowsLayerRotate: true,
|
||||
},
|
||||
{
|
||||
id: "hand",
|
||||
interactionMode: "pan",
|
||||
allowsLayerDrag: false,
|
||||
allowsLayerResize: false,
|
||||
allowsLayerRotate: false,
|
||||
},
|
||||
{
|
||||
id: "face",
|
||||
interactionMode: "annotate",
|
||||
allowsLayerDrag: false,
|
||||
allowsLayerResize: false,
|
||||
allowsLayerRotate: false,
|
||||
},
|
||||
{
|
||||
id: "fill",
|
||||
interactionMode: "paint",
|
||||
allowsLayerDrag: false,
|
||||
allowsLayerResize: false,
|
||||
allowsLayerRotate: false,
|
||||
},
|
||||
{
|
||||
id: "brush",
|
||||
interactionMode: "paint",
|
||||
allowsLayerDrag: false,
|
||||
allowsLayerResize: false,
|
||||
allowsLayerRotate: false,
|
||||
},
|
||||
];
|
||||
|
||||
const toolRegistry = new Map<string, ToolDefinition>(
|
||||
definitions.map((def) => [def.id, def]),
|
||||
);
|
||||
|
||||
export function getToolDefinition(id: string): ToolDefinition | undefined {
|
||||
return toolRegistry.get(id);
|
||||
}
|
||||
|
||||
export function getAllTools(): ToolDefinition[] {
|
||||
return definitions;
|
||||
}
|
||||
|
||||
export type EditorToolId = (typeof definitions)[number]["id"];
|
||||
Reference in New Issue
Block a user