Files
image-studio/commands/transform.ts
syntaxbullet 5e4b548ad4 feat: add ComfyUI integration for image generation and management
- Implemented ComfyGenerateRequest type and associated functions for generating images using various architectures and modes.
- Added functions for listing generation options and handling image uploads.
- Created workflows for different generation modes including SDXL, Z-Image, Z-Image Turbo, and Anima.
- Introduced GenerationJobStatus component to display the status of ongoing generation jobs.
- Developed MaskControls for managing mask operations and displaying mask analysis.
- Created palette items for tool selection, layer management, and generation settings.
2026-07-10 23:15:02 +02:00

157 lines
4.5 KiB
TypeScript

import type { Rect, Vec2D } from "@core/geometry";
import { applyTransformTargetBounds } from "./transform-document";
import type { TransformHandle, TransformTarget } from "@editor/transform";
import type { Command } from "./command";
import { commandIds } from "./ids";
export type TransformBeginPayload = {
target: TransformTarget;
handle: TransformHandle;
point: Vec2D;
initialBounds: Rect;
};
export type TransformUpdatePayload = {
point: Vec2D;
shiftKey?: boolean;
};
export type TransformSetBoundsPayload = {
target: TransformTarget;
bounds: Rect;
};
export const transformBeginCommand: Command<TransformBeginPayload> = {
id: commandIds.transformBegin,
name: "Begin transform",
history: { mode: "deferred", phase: "begin" },
execute({ state }, payload) {
return {
...state,
editor: {
...state.editor,
transformSession: {
target: payload.target,
handle: payload.handle,
startPoint: payload.point,
initialBounds: payload.initialBounds,
},
},
};
},
};
export const transformUpdateCommand: Command<TransformUpdatePayload> = {
id: commandIds.transformUpdate,
name: "Update transform",
history: { mode: "deferred", phase: "update" },
execute({ state }, payload) {
const session = state.editor.transformSession;
if (!session) return state;
const nextBounds = transformBounds(
session.initialBounds,
session.handle,
{
x: payload.point.x - session.startPoint.x,
y: payload.point.y - session.startPoint.y,
},
payload.shiftKey === true,
);
return {
...state,
document: applyTransformTargetBounds(state.document, session.target, nextBounds),
};
},
};
export const transformSetBoundsCommand: Command<TransformSetBoundsPayload> = {
id: commandIds.transformSetBounds,
name: "Set transform bounds",
execute({ state }, payload) {
return {
...state,
document: applyTransformTargetBounds(state.document, payload.target, normalizeRect(payload.bounds)),
};
},
};
export const transformEndCommand: Command = {
id: commandIds.transformEnd,
name: "End transform",
history: { mode: "deferred", phase: "commit" },
execute({ state }) {
if (!state.editor.transformSession) return state;
return {
...state,
editor: {
...state.editor,
transformSession: undefined,
},
};
},
};
export const transformCommands = [transformBeginCommand, transformUpdateCommand, transformSetBoundsCommand, transformEndCommand] satisfies Command<unknown>[];
function transformBounds(bounds: Rect, handle: TransformHandle, delta: Vec2D, constrained = false): Rect {
if (handle === "body") {
const constrainedDelta = constrained ? constrainMoveDelta(delta) : delta;
return { ...bounds, x: bounds.x + constrainedDelta.x, y: bounds.y + constrainedDelta.y };
}
let x = bounds.x;
let y = bounds.y;
let w = bounds.w;
let h = bounds.h;
if (handle.includes("w")) {
x = bounds.x + delta.x;
w = bounds.w - delta.x;
}
if (handle.includes("e")) w = bounds.w + delta.x;
if (handle.includes("n")) {
y = bounds.y + delta.y;
h = bounds.h - delta.y;
}
if (handle.includes("s")) h = bounds.h + delta.y;
const resized = normalizeRect({ x, y, w, h });
if (!constrained || !isCornerHandle(handle)) return resized;
return proportionalCornerResize(bounds, handle, resized);
}
function constrainMoveDelta(delta: Vec2D): Vec2D {
return Math.abs(delta.x) >= Math.abs(delta.y) ? { x: delta.x, y: 0 } : { x: 0, y: delta.y };
}
function isCornerHandle(handle: TransformHandle) {
return handle.length === 2;
}
function proportionalCornerResize(initial: Rect, handle: TransformHandle, resized: Rect): Rect {
const aspectRatio = initial.w / initial.h;
const widthScale = resized.w / initial.w;
const heightScale = resized.h / initial.h;
const scale = Math.abs(widthScale - 1) >= Math.abs(heightScale - 1) ? widthScale : heightScale;
const w = Math.max(1, initial.w * scale);
const h = Math.max(1, w / aspectRatio);
const x = handle.includes("w") ? initial.x + initial.w - w : initial.x;
const y = handle.includes("n") ? initial.y + initial.h - h : initial.y;
return { x, y, w, h };
}
function normalizeRect(rect: Rect): Rect {
const minSize = 1;
return {
x: rect.w < minSize ? rect.x + rect.w - minSize : rect.x,
y: rect.h < minSize ? rect.y + rect.h - minSize : rect.y,
w: Math.max(minSize, rect.w),
h: Math.max(minSize, rect.h),
};
}