feat(canvas): enhance multi-image generation controls and improve image preview behavior

This commit is contained in:
HouYunFei
2026-08-18 14:53:43 +08:00
parent 2c220f7435
commit fadba9f7a5
4 changed files with 30 additions and 28 deletions
+16 -22
View File
@@ -53,7 +53,7 @@ type CanvasNodeProps = {
onDeleteBatchImage?: (nodeId: string, imageId: string) => void;
onRetry?: (node: CanvasNodeData) => void;
onGenerateImage?: (node: CanvasNodeData) => void;
onViewImage?: (node: CanvasNodeData) => void;
onViewImage?: (node: CanvasNodeData, imageId?: string) => void;
onContextMenu: (event: React.MouseEvent, nodeId: string) => void;
};
@@ -78,6 +78,7 @@ type NodeContentRendererProps = {
onDownloadBatchImage?: (imageId: string) => void;
onRetryBatchImage?: (imageId: string) => void;
onDeleteBatchImage?: (imageId: string) => void;
onViewBatchImage?: (imageId: string) => void;
groupChildCount: number;
};
@@ -365,11 +366,6 @@ export const CanvasNode = React.memo(function CanvasNode({
}}
onMouseDown={(event) => onMouseDown(event, data.id)}
onDoubleClick={(event) => {
if (isBatchRoot) {
event.stopPropagation();
onToggleBatch?.(data.id);
return;
}
if (definition?.onDoubleClick && pluginContext) {
if (definition.onDoubleClick(pluginContext)) event.stopPropagation();
return;
@@ -414,6 +410,7 @@ export const CanvasNode = React.memo(function CanvasNode({
onDownloadBatchImage={(imageId) => onDownloadBatchImage?.(data, imageId)}
onRetryBatchImage={(imageId) => onRetryBatchImage?.(data, imageId)}
onDeleteBatchImage={(imageId) => onDeleteBatchImage?.(data.id, imageId)}
onViewBatchImage={(imageId) => onViewImage?.(data, imageId)}
groupChildCount={groupChildCount}
/>
</div>
@@ -590,6 +587,7 @@ function ImageNodeContent(props: NodeContentRendererProps) {
onDownloadBatchImage={props.onDownloadBatchImage}
onRetryBatchImage={props.onRetryBatchImage}
onDeleteBatchImage={props.onDeleteBatchImage}
onViewBatchImage={props.onViewBatchImage}
/>
);
}
@@ -647,6 +645,7 @@ function ImageContent({
onDownloadBatchImage,
onRetryBatchImage,
onDeleteBatchImage,
onViewBatchImage,
}: {
node: CanvasNodeData;
batchExpanded: boolean;
@@ -656,6 +655,7 @@ function ImageContent({
onDownloadBatchImage?: (imageId: string) => void;
onRetryBatchImage?: (imageId: string) => void;
onDeleteBatchImage?: (imageId: string) => void;
onViewBatchImage?: (imageId: string) => void;
}) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const { t } = useTranslation();
@@ -667,11 +667,11 @@ function ImageContent({
const primaryContent = primaryImage?.content || node.metadata?.content;
return (
<BatchFrame batchCount={batchCount} batchExpanded={batchExpanded} onToggleBatch={onToggleBatch}>
<BatchFrame batchCount={batchCount} batchExpanded={batchExpanded}>
{batchExpanded
? images
.filter((image) => image.id !== primaryImageId)
.map((image, index) => <ExpandedImageCard key={image.id} node={node} image={image} index={index} onSetPrimary={() => onSetBatchPrimary?.(image.id)} onDuplicate={() => onDuplicateBatchImage?.(image.id)} onDownload={() => onDownloadBatchImage?.(image.id)} onRetry={() => onRetryBatchImage?.(image.id)} onDelete={() => onDeleteBatchImage?.(image.id)} />)
.map((image, index) => <ExpandedImageCard key={image.id} node={node} image={image} index={index} onView={() => onViewBatchImage?.(image.id)} onSetPrimary={() => onSetBatchPrimary?.(image.id)} onDuplicate={() => onDuplicateBatchImage?.(image.id)} onDownload={() => onDownloadBatchImage?.(image.id)} onRetry={() => onRetryBatchImage?.(image.id)} onDelete={() => onDeleteBatchImage?.(image.id)} />)
: null}
<div className="h-full w-full overflow-hidden rounded-3xl">
{primaryContent ? (
@@ -714,7 +714,7 @@ function ImageContent({
);
}
function ExpandedImageCard({ node, image, index, onSetPrimary, onDuplicate, onDownload, onRetry, onDelete }: { node: CanvasNodeData; image: CanvasNodeImage; index: number; onSetPrimary: () => void; onDuplicate: () => void; onDownload: () => void; onRetry: () => void; onDelete: () => void }) {
function ExpandedImageCard({ node, image, index, onView, onSetPrimary, onDuplicate, onDownload, onRetry, onDelete }: { node: CanvasNodeData; image: CanvasNodeImage; index: number; onView: () => void; onSetPrimary: () => void; onDuplicate: () => void; onDownload: () => void; onRetry: () => void; onDelete: () => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const { t } = useTranslation();
const count = node.metadata?.images?.length || 0;
@@ -746,7 +746,11 @@ function ExpandedImageCard({ node, image, index, onSetPrimary, onDuplicate, onDo
}
onMouseDown={(event) => event.stopPropagation()}
onPointerDown={(event) => event.stopPropagation()}
onDoubleClick={(event) => event.stopPropagation()}
onDoubleClick={(event) => {
if (!image.content || (event.target instanceof Element && event.target.closest("button"))) return;
event.stopPropagation();
onView();
}}
>
{image.content ? <img src={image.content} alt={node.title} draggable={false} className="pointer-events-none h-full w-full select-none object-contain" /> : <ImageSlotStatus image={image} />}
{image.content ? (
@@ -812,21 +816,11 @@ function ImageInfoBar({ node }: { node: CanvasNodeData }) {
);
}
function BatchFrame({ batchCount, batchExpanded, onToggleBatch, children }: { batchCount: number; batchExpanded: boolean; onToggleBatch?: () => void; children: ReactNode }) {
function BatchFrame({ batchCount, batchExpanded, children }: { batchCount: number; batchExpanded: boolean; children: ReactNode }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const isBatchRoot = batchCount > 1;
return (
<div
className="group/batch relative h-full w-full overflow-visible"
onDoubleClick={
isBatchRoot
? (event) => {
event.stopPropagation();
onToggleBatch?.();
}
: undefined
}
>
<div className="group/batch relative h-full w-full overflow-visible">
{isBatchRoot ? (
<div className="pointer-events-none absolute inset-0 overflow-visible">
{Array.from({ length: Math.min(batchCount - 1, 3) }).map((_, index) => (
+9 -4
View File
@@ -232,6 +232,7 @@ function InfiniteCanvasPage() {
const [superResolveNodeId, setSuperResolveNodeId] = useState<string | null>(null);
const [angleNodeId, setAngleNodeId] = useState<string | null>(null);
const [previewNodeId, setPreviewNodeId] = useState<string | null>(null);
const [previewImageId, setPreviewImageId] = useState<string | null>(null);
const [titleEditing, setTitleEditing] = useState(false);
const [titleDraft, setTitleDraft] = useState("");
const [historyState, setHistoryState] = useState({ canUndo: false, canRedo: false });
@@ -590,6 +591,7 @@ function InfiniteCanvasPage() {
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 previewContent = previewImageId ? previewNode?.metadata?.images?.find((image) => image.id === previewImageId)?.content : previewNode?.metadata?.content;
const hasMultipleSelectedNodes = selectedNodeIds.size > 1;
const activeNodeId = hasMultipleSelectedNodes ? null : hoveredNodeId || (selectedNodeIds.size === 1 ? Array.from(selectedNodeIds)[0] : null);
const groupChildCountById = useMemo(() => {
@@ -2693,7 +2695,10 @@ function InfiniteCanvasPage() {
const handleNodeHoverEnd = useCallback((nodeId: string) => {
setHoveredNodeId((current) => (current === nodeId ? null : current));
}, []);
const handleNodeViewImage = useCallback((node: CanvasNodeData) => setPreviewNodeId(node.id), []);
const handleNodeViewImage = useCallback((node: CanvasNodeData, imageId?: string) => {
setPreviewNodeId(node.id);
setPreviewImageId(imageId || null);
}, []);
const handleNodeRetry = useCallback((node: CanvasNodeData) => void handleRetryNode(node), [handleRetryNode]);
const handleNodeContextMenu = useCallback((event: ReactMouseEvent, nodeId: string) => {
event.preventDefault();
@@ -2918,7 +2923,7 @@ function InfiniteCanvasPage() {
onUpscale={(node) => setUpscaleNodeId(node.id)}
onSuperResolve={(node) => setSuperResolveNodeId(node.id)}
onAngle={(node) => setAngleNodeId(node.id)}
onViewImage={(node) => setPreviewNodeId(node.id)}
onViewImage={handleNodeViewImage}
onReversePrompt={createImageReversePromptNodes}
onRetry={(node) => void handleRetryNode(node)}
onToggleFreeResize={(node) => toggleNodeFreeResize(node.id)}
@@ -2998,14 +3003,14 @@ function InfiniteCanvasPage() {
<Modal
title={t("canvas.projectPage.imageDetails")}
open={Boolean(previewNode?.metadata?.content)}
open={Boolean(previewContent)}
centered
onCancel={() => setPreviewNodeId(null)}
footer={null}
width="auto"
styles={{ body: { padding: 0, display: "flex", justifyContent: "center", alignItems: "center", maxHeight: "80vh" } }}
>
{previewNode?.metadata?.content ? <img src={previewNode.metadata.content} alt={previewNode.title || t("assets.kinds.image")} style={{ maxWidth: "100%", maxHeight: "80vh", objectFit: "contain" }} /> : null}
{previewContent ? <img src={previewContent} alt={previewNode?.title || t("assets.kinds.image")} style={{ maxWidth: "100%", maxHeight: "80vh", objectFit: "contain" }} /> : null}
</Modal>
<Modal