You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

155 lines
5.9 KiB

"use client";
import { useEffect, useMemo, useState, type ReactNode } from "react";
import { useViewport } from "@xyflow/react";
import { GenerationComposer } from "@/components/composer/GenerationComposer";
import { NodeHighDefinitionPanel } from "@/components/composer/NodeHighDefinitionPanel";
import { useSelectedNodeCount } from "@/hooks/useSelectedNodeCount";
import { getActiveNodeTool, useNodeToolStore, type NodeToolType } from "@/store/nodeToolStore";
import { useWorkflowStore } from "@/store/workflowStore";
import type { WorkflowNode } from "@/types";
import { deriveComposerContextForNodeId } from "@/utils/composerNodeDescriptor";
import { getNodeImageSource } from "@/utils/getNodeImageSource";
import { isSmartImageAutoTaskNode } from "@/utils/smartMediaMode";
import { isHighDefinitionDerivedImageNode } from "@/utils/highDefinitionNodes";
interface NodeGenerationComposerProps {
nodeId: string;
selected: boolean;
}
function isToolSupported(tool: NodeToolType, node: WorkflowNode | null): boolean {
if (tool === "multiAngle") return Boolean(getNodeImageSource(node));
if (tool === "highDefinition") return Boolean(node && isHighDefinitionDerivedImageNode(node.data));
if (tool === "generationComposer") return true;
if (tool === "crop" || tool === "splitGrid" || tool === "outpainting" || tool === "inpainting" || tool === "annotate") {
return Boolean(getNodeImageSource(node));
}
return false;
}
function isAutoImageTaskNode(node: WorkflowNode | null): boolean {
return isSmartImageAutoTaskNode(node?.data);
}
export function NodeGenerationComposer({
nodeId,
selected,
}: NodeGenerationComposerProps) {
return <EditableNodeGenerationComposer nodeId={nodeId} selected={selected} />;
}
function NodeGenerationComposerFrame({
hidden,
children,
}: {
hidden: boolean;
children: ReactNode;
}) {
const { zoom } = useViewport();
const safeZoom = zoom > 0 ? zoom : 1;
return (
<div
data-canvas-generator-root
hidden={hidden}
className="pointer-events-none absolute left-0 top-0 h-full w-full overflow-visible"
>
<div
className="node-floating-ui nodrag nopan nowheel pointer-events-auto absolute left-1/2 top-full z-[10020]"
style={{
transform: `translateX(-50%) translateY(${12 / safeZoom}px) scale(${1 / safeZoom})`,
transformOrigin: "top center",
}}
>
{children}
</div>
</div>
);
}
function EditableNodeGenerationComposer({ nodeId, selected }: Pick<NodeGenerationComposerProps, "nodeId" | "selected">) {
const selectedNodeCount = useSelectedNodeCount();
const nodes = useWorkflowStore((state) => state.nodes);
const edges = useWorkflowStore((state) => state.edges);
const activeTool = useNodeToolStore((state) => getActiveNodeTool(state, nodeId));
const closeToolForNode = useNodeToolStore((state) => state.closeToolForNode);
const context = useMemo(
() => deriveComposerContextForNodeId(nodes, edges, nodeId),
[edges, nodes, nodeId]
);
const hideForAutoTask = isAutoImageTaskNode(context.node);
const isHighDefinitionNode = Boolean(context.node && isHighDefinitionDerivedImageNode(context.node.data));
const isImageInputComposer = context.mode === "unsupported-node" && context.node?.type === "imageInput";
const shouldShowComposer =
selected &&
!hideForAutoTask &&
selectedNodeCount === 1 &&
(context.mode === "node-edit" || context.mode === "process-node" || isImageInputComposer);
const [shouldMountComposer, setShouldMountComposer] = useState(shouldShowComposer);
const canShowNodeFloatingUi = selected && selectedNodeCount === 1;
// multiAngle 由画布级宿主(ActiveImageEditHost)接管,这里不再处理。
const requestedTool = canShowNodeFloatingUi && activeTool !== "multiAngle" ? activeTool : null;
const shouldShowHighDefinitionPanel = selected && selectedNodeCount === 1 && isHighDefinitionNode;
const derivedTool: NodeToolType | null = shouldShowComposer
? "generationComposer"
: shouldShowHighDefinitionPanel
? "highDefinition"
: null;
const currentTool = requestedTool
? isToolSupported(requestedTool, context.node)
? requestedTool
: null
: derivedTool && isToolSupported(derivedTool, context.node)
? derivedTool
: null;
const shouldShowComposerUi = currentTool === "generationComposer";
const isComposerHidden = !shouldShowComposerUi;
const shouldShowHighDefinitionUi = currentTool === "highDefinition";
const isFrameHidden = !shouldShowComposerUi && !shouldShowHighDefinitionUi;
const isComposerMounted = shouldMountComposer || shouldShowComposer;
useEffect(() => {
if (shouldShowComposer) setShouldMountComposer(true);
}, [shouldShowComposer]);
useEffect(() => {
if (canShowNodeFloatingUi) return;
closeToolForNode(nodeId);
}, [activeTool, canShowNodeFloatingUi, closeToolForNode, nodeId]);
useEffect(() => {
if (isComposerHidden) {
if (activeTool === "generationComposer") closeToolForNode(nodeId);
}
}, [activeTool, closeToolForNode, isComposerHidden, nodeId]);
useEffect(() => {
if (!requestedTool || currentTool === requestedTool) return;
closeToolForNode(nodeId);
}, [closeToolForNode, currentTool, nodeId, requestedTool]);
if (hideForAutoTask && !shouldShowHighDefinitionPanel) {
return null;
}
return (
<NodeGenerationComposerFrame hidden={isFrameHidden}>
{shouldShowHighDefinitionUi && context.node ? (
<NodeHighDefinitionPanel node={context.node} />
) : null}
{isComposerMounted ? (
<div hidden={isComposerHidden} className="pointer-events-auto flex flex-col items-center gap-2">
<GenerationComposer
targetNodeId={nodeId}
embedded
isHidden={isComposerHidden}
onHiddenPersisted={() => {
setShouldMountComposer(false);
}}
/>
</div>
) : null}
</NodeGenerationComposerFrame>
);
}