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
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>
|
|
);
|
|
}
|
|
|