feat(canvas): add image upscaling and super resolution features with UI enhancements

This commit is contained in:
HouYunFei
2026-06-03 16:05:35 +08:00
parent 76a4e7c4a2
commit ceb7605a1e
5 changed files with 218 additions and 4 deletions
@@ -19,7 +19,7 @@ import { canvasThemes, type CanvasBackgroundMode } from "@/lib/canvas-theme";
import { UserStatusActions } from "@/components/layout/user-status-actions";
import { useAssetStore } from "@/stores/use-asset-store";
import { useThemeStore } from "@/stores/use-theme-store";
import { cropDataUrl } from "../utils/canvas-image-data";
import { cropDataUrl, upscaleDataUrl } from "../utils/canvas-image-data";
import { fitNodeSize, nodeSizeFromRatio } from "../utils/canvas-node-size";
import { App, Button, Dropdown, Modal } from "antd";
import { NODE_DEFAULT_SIZE, getNodeSpec } from "../constants";
@@ -29,6 +29,7 @@ import { CanvasAssistantPanel } from "../components/canvas-assistant-panel";
import { CanvasNodeContextMenu } from "../components/canvas-context-menu";
import { CanvasNodeAngleDialog, type CanvasImageAngleParams } from "../components/canvas-node-angle-dialog";
import { CanvasNodeCropDialog, type CanvasImageCropRect } from "../components/canvas-node-crop-dialog";
import { CanvasNodeUpscaleDialog, type CanvasImageUpscaleParams } from "../components/canvas-node-upscale-dialog";
import { buildNodeChatMessages, buildNodeGenerationContext, buildNodeGenerationInputs, hydrateNodeGenerationContext, type NodeGenerationInput } from "../components/canvas-node-generation";
import { CanvasNodeHoverToolbar, CanvasNodeInfoModal } from "../components/canvas-node-hover-toolbar";
import { InfiniteCanvas } from "../components/infinite-canvas";
@@ -272,6 +273,8 @@ function InfiniteCanvasPage() {
const [editRequestNonce, setEditRequestNonce] = useState(0);
const [infoNodeId, setInfoNodeId] = useState<string | null>(null);
const [cropNodeId, setCropNodeId] = useState<string | null>(null);
const [upscaleNodeId, setUpscaleNodeId] = useState<string | null>(null);
const [superResolveNodeId, setSuperResolveNodeId] = useState<string | null>(null);
const [angleNodeId, setAngleNodeId] = useState<string | null>(null);
const [previewNodeId, setPreviewNodeId] = useState<string | null>(null);
const [assistantCollapsed, setAssistantCollapsed] = useState(true);
@@ -573,6 +576,8 @@ function InfiniteCanvasPage() {
const toolbarNode = toolbarNodeId ? nodeById.get(toolbarNodeId) || null : null;
const infoNode = infoNodeId ? nodeById.get(infoNodeId) || null : null;
const cropNode = cropNodeId ? nodeById.get(cropNodeId) || null : null;
const upscaleNode = upscaleNodeId ? nodeById.get(upscaleNodeId) || null : null;
const superResolveNode = superResolveNodeId ? nodeById.get(superResolveNodeId) || null : null;
const angleNode = angleNodeId ? nodeById.get(angleNodeId) || null : null;
const previewNode = previewNodeId ? nodeById.get(previewNodeId) || null : null;
const hasMultipleSelectedNodes = selectedNodeIds.size > 1;
@@ -1459,6 +1464,31 @@ function InfiniteCanvasPage() {
setCropNodeId(null);
}, []);
const upscaleImageNode = useCallback(async (node: CanvasNodeData, params: CanvasImageUpscaleParams) => {
if (!node.metadata?.content) return;
setUpscaleNodeId(null);
const upscaled = await upscaleDataUrl(node.metadata.content, params);
const image = await uploadImage(upscaled);
const size = fitNodeSize(image.width, image.height);
const childId = nanoid();
const child: CanvasNodeData = {
id: childId,
type: CanvasNodeType.Image,
title: "Upscaled Image",
position: { x: node.position.x + node.width + 96, y: node.position.y },
width: size.width,
height: size.height,
metadata: {
...imageMetadata(image),
prompt: node.metadata?.prompt,
},
};
setNodes((prev) => [...prev, child]);
setConnections((prev) => [...prev, { id: nanoid(), fromNodeId: node.id, toNodeId: childId }]);
setSelectedNodeIds(new Set([childId]));
setDialogNodeId(childId);
}, []);
const generateAngleNode = useCallback(
async (node: CanvasNodeData, params: CanvasImageAngleParams) => {
if (!node.metadata?.content) return;
@@ -2295,6 +2325,8 @@ function InfiniteCanvasPage() {
onDownload={downloadNodeImage}
onSaveAsset={(node) => void saveNodeAsset(node)}
onCrop={(node) => setCropNodeId(node.id)}
onUpscale={(node) => setUpscaleNodeId(node.id)}
onSuperResolve={(node) => setSuperResolveNodeId(node.id)}
onAngle={(node) => setAngleNodeId(node.id)}
onViewImage={(node) => setPreviewNodeId(node.id)}
onRetry={(node) => void handleRetryNode(node)}
@@ -2361,6 +2393,12 @@ function InfiniteCanvasPage() {
{cropNode?.metadata?.content ? <CanvasNodeCropDialog dataUrl={cropNode.metadata.content} open={Boolean(cropNode)} onClose={() => setCropNodeId(null)} onConfirm={(crop) => void cropImageNode(cropNode!, crop)} /> : null}
{upscaleNode?.metadata?.content ? <CanvasNodeUpscaleDialog dataUrl={upscaleNode.metadata.content} open={Boolean(upscaleNode)} onClose={() => setUpscaleNodeId(null)} onConfirm={(params) => void upscaleImageNode(upscaleNode!, params)} /> : null}
<Modal title="AI 超分" open={Boolean(superResolveNode?.metadata?.content)} centered footer={null} onCancel={() => setSuperResolveNodeId(null)}>
<div className="py-8 text-center text-base font-medium"></div>
</Modal>
{angleNode?.metadata?.content ? <CanvasNodeAngleDialog dataUrl={angleNode.metadata.content} open={Boolean(angleNode)} onClose={() => setAngleNodeId(null)} onConfirm={(params) => void generateAngleNode(angleNode!, params)} /> : null}
<Modal