feat: add ComfyUI integration for image generation
- Implemented ComfyUI API for generating images with various modes (text-to-image, image-to-image, inpaint, outpaint). - Created GenerateSheet and associated controls for user input on generation settings. - Added subtle scrollbar styles for improved UI experience. - Enhanced canvas input handling to ignore key events when focused on editable elements. - Optimized canvas resizing logic to prevent unnecessary dispatches. - Introduced error handling for generation failures and loading models. - Added functionality to upload images and masks for inpainting.
This commit is contained in:
58
view/App.tsx
58
view/App.tsx
@@ -1,14 +1,16 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { DownloadSimple, FolderOpen, Stack } from "@phosphor-icons/react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { DownloadSimple, FolderOpen, Sparkle, Stack } from "@phosphor-icons/react";
|
||||
import type { ImageStudioApp } from "@app/app";
|
||||
import { commandIds } from "@commands/ids";
|
||||
import { BottomControlsIsland } from "./BottomControlsIsland";
|
||||
import { brushUnavailableHint } from "./canvas/brush";
|
||||
import { CanvasViewport } from "./CanvasViewport";
|
||||
import { GenerateSheet } from "./GenerateSheet";
|
||||
import { LayersSheet } from "./LayersSheet";
|
||||
import { ShortcutsDisplay } from "./ShortcutsDisplay";
|
||||
import { ToolOverlay } from "./ToolOverlay";
|
||||
import { resolveTransformTargetBounds, selectedTransformTarget } from "@editor/transform-targets";
|
||||
import type { ToolId } from "@editor/tools";
|
||||
import { handleDeleteSelectionKey, handleHistoryKey, handleToolKey, keybindEventFromKeyboardEvent } from "@input/index";
|
||||
import { useAppState } from "./useAppState";
|
||||
import { downloadArtboardPng } from "./exportArtboardPng";
|
||||
@@ -25,9 +27,31 @@ export function App({ app }: AppProps) {
|
||||
const viewportActivityIsland = useViewportActivityIsland(state.editor.viewport);
|
||||
const imageImport = useImageImport(app.store);
|
||||
const [layersOpen, setLayersOpen] = useState(false);
|
||||
const [generateOpen, setGenerateOpen] = useState(false);
|
||||
const previousGenerateTool = useRef<ToolId>("select");
|
||||
const transformTarget = state.editor.transformSession?.target ?? selectedTransformTarget(state.document, state.editor.selection);
|
||||
const activeArtboard = state.document.artboards.find((artboard) => artboard.id === state.editor.selection.artboardId) ?? state.document.artboards[0];
|
||||
|
||||
const openGenerate = useCallback(() => {
|
||||
const activeTool = app.store.getState().editor.tools.activeTool;
|
||||
if (activeTool !== "generate") previousGenerateTool.current = activeTool;
|
||||
app.store.dispatch(commandIds.toolSetActive, { tool: "generate" });
|
||||
setLayersOpen(false);
|
||||
setGenerateOpen(true);
|
||||
}, [app.store]);
|
||||
|
||||
const closeGenerate = useCallback(() => {
|
||||
setGenerateOpen(false);
|
||||
if (app.store.getState().editor.tools.activeTool === "generate") {
|
||||
app.store.dispatch(commandIds.toolSetActive, { tool: previousGenerateTool.current });
|
||||
}
|
||||
}, [app.store]);
|
||||
|
||||
const toggleGenerate = useCallback(() => {
|
||||
if (generateOpen) closeGenerate();
|
||||
else openGenerate();
|
||||
}, [closeGenerate, generateOpen, openGenerate]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const target = event.target;
|
||||
@@ -51,16 +75,23 @@ export function App({ app }: AppProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = event.key.toLowerCase();
|
||||
const keybindEvent = keybindEventFromKeyboardEvent(event);
|
||||
|
||||
if (key === "g") {
|
||||
toggleGenerate();
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const toolConsumed = handleToolKey({ event: keybindEvent, dispatch: app.store.dispatch });
|
||||
if (toolConsumed) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const key = event.key.toLowerCase();
|
||||
|
||||
if (key === "l") {
|
||||
closeGenerate();
|
||||
setLayersOpen((open) => !open);
|
||||
event.preventDefault();
|
||||
return;
|
||||
@@ -76,7 +107,7 @@ export function App({ app }: AppProps) {
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [app.store]);
|
||||
}, [app.store, closeGenerate, toggleGenerate]);
|
||||
const transformBounds = transformTarget ? resolveTransformTargetBounds(state.document, transformTarget) : undefined;
|
||||
const brushHint = brushUnavailableHint(state.document, state.editor);
|
||||
|
||||
@@ -88,10 +119,13 @@ export function App({ app }: AppProps) {
|
||||
<button type="button" className={topBarButtonClass()} onClick={imageImport.openFilePicker}>
|
||||
<FolderOpen size={24} />
|
||||
</button>
|
||||
<button type="button" className={topBarButtonClass(generateOpen)} aria-pressed={generateOpen} onClick={toggleGenerate}>
|
||||
<Sparkle size={24} weight={generateOpen ? "fill" : "regular"} />
|
||||
</button>
|
||||
<button type="button" className={topBarButtonClass()} disabled={!activeArtboard} onClick={() => activeArtboard && void downloadArtboardPng(activeArtboard, state.document.assets)}>
|
||||
<DownloadSimple size={24} />
|
||||
</button>
|
||||
<button type="button" className={topBarButtonClass(layersOpen)} aria-pressed={layersOpen} onClick={() => setLayersOpen((open) => !open)}>
|
||||
<button type="button" className={topBarButtonClass(layersOpen)} aria-pressed={layersOpen} onClick={() => { closeGenerate(); setLayersOpen((open) => !open); }}>
|
||||
<Stack size={24} weight={layersOpen ? "fill" : "regular"} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -103,6 +137,15 @@ export function App({ app }: AppProps) {
|
||||
dispatch={app.store.dispatch}
|
||||
/>
|
||||
</div>
|
||||
<GenerateSheet
|
||||
document={state.document}
|
||||
selection={state.editor.selection}
|
||||
viewport={state.editor.viewport}
|
||||
settings={state.editor.tools.generate}
|
||||
open={generateOpen}
|
||||
onOpenChange={(open) => open ? openGenerate() : closeGenerate()}
|
||||
dispatch={app.store.dispatch}
|
||||
/>
|
||||
<LayersSheet
|
||||
document={state.document}
|
||||
selection={state.editor.selection}
|
||||
@@ -115,10 +158,11 @@ export function App({ app }: AppProps) {
|
||||
document={state.document}
|
||||
selection={state.editor.selection}
|
||||
viewport={state.editor.viewport}
|
||||
visible={state.editor.tools.activeTool === "brush" || state.editor.tools.activeTool === "eraser" || state.editor.tools.activeTool === "chromaKey" || state.editor.tools.activeTool === "magicWand" || Boolean(transformBounds) || viewportActivityIsland.visible}
|
||||
visible={state.editor.tools.activeTool === "generate" || state.editor.tools.activeTool === "brush" || state.editor.tools.activeTool === "eraser" || state.editor.tools.activeTool === "chromaKey" || state.editor.tools.activeTool === "magicWand" || Boolean(transformBounds) || viewportActivityIsland.visible}
|
||||
action={viewportActivityIsland.action}
|
||||
activeTool={state.editor.tools.activeTool}
|
||||
brushSettings={state.editor.tools.brush}
|
||||
generateSettings={state.editor.tools.generate}
|
||||
chromaKeySettings={state.editor.tools.chromaKey}
|
||||
magicWandSettings={state.editor.tools.magicWand}
|
||||
editingMask={Boolean(state.editor.maskEdit)}
|
||||
|
||||
Reference in New Issue
Block a user