Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/src/content/docs/features/Workflows/editor-interface.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ If you're not familiar with Diffusion, take a look at our [Diffusion Overview](.
<Card title="Node Caching" icon="rocket">
Nodes have a **"Use Cache"** option in their footer. This allows for performance improvements by reusing previously cached values during workflow processing.
</Card>
<Card title="Workflow Image Export" icon="download">
Click the camera button in the workflow editor controls to download a PNG of the complete workflow, rendered at up to 2x resolution. The image includes connectors, the background grid, and the selected Invoke color scheme, with nodes shown deselected and the MiniMap omitted.
</Card>

### Managing Nodes

Expand Down
1 change: 1 addition & 0 deletions invokeai/frontend/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"filesize": "^10.1.6",
"fracturedjsonjs": "^4.1.1",
"framer-motion": "^11.18.2",
"html-to-image": "^1.11.13",
"i18next": "^25.7.3",
"i18next-http-backend": "^3.0.2",
"idb-keyval": "6.2.1",
Expand Down
8 changes: 8 additions & 0 deletions invokeai/frontend/web/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1550,6 +1550,8 @@
"currentImageDescription": "Displays the current image in the Node Editor",
"downloadWorkflow": "Download Workflow JSON",
"downloadWorkflowError": "Error downloading workflow",
"downloadWorkflowImage": "Download Workflow Image",
"downloadWorkflowImageError": "Error downloading workflow image",
"edge": "Edge",
"edit": "Edit",
"editMode": "Edit in Workflow Editor",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import { getConnectorDeletionSpliceConnections } from 'features/nodes/store/util
import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil';
import { validateConnection } from 'features/nodes/store/util/validateConnection';
import { selectSelectionMode, selectShouldSnapToGrid } from 'features/nodes/store/workflowSettingsSlice';
import { NO_DRAG_CLASS, NO_PAN_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import { NO_DRAG_CLASS, NO_PAN_CLASS, NO_WHEEL_CLASS, WORKFLOW_GRID_SIZE } from 'features/nodes/types/constants';
import type { AnyEdge, AnyNode } from 'features/nodes/types/invocation';
import { buildConnectorNode } from 'features/nodes/util/node/buildConnectorNode';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
Expand Down Expand Up @@ -88,7 +88,7 @@ const nodeTypes = {
// TODO: can we support reactflow? if not, we could style the attribution so it matches the app
const proOptions: ProOptions = { hideAttribution: true };

const snapGrid: [number, number] = [25, 25];
const snapGrid: [number, number] = [WORKFLOW_GRID_SIZE, WORKFLOW_GRID_SIZE];

const selectCancelConnection = (state: ReactFlowState) => state.cancelConnection;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,15 @@ const ConnectorNode = ({ id, selected }: NodeProps<Node<ConnectorNodeData>>) =>
justifyContent="center"
borderRadius="full"
bg={selected ? 'base.650' : 'base.700'}
data-connector-node-body="true"
>
<Box className="connector-border" />
<Icon as={PiDotOutlineFill} boxSize={5} color={selected ? 'base.50' : 'base.100'} />
<Icon
as={PiDotOutlineFill}
boxSize={5}
color={selected ? 'base.50' : 'base.100'}
data-connector-node-icon="true"
/>
</Box>
<Handle
className={NO_DRAG_CLASS}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface Props {
export const InvocationNodeInfoIcon = memo(({ nodeId }: Props) => {
return (
<Tooltip label={<TooltipContent nodeId={nodeId} />} placement="top" shouldWrapChildren>
<Icon as={PiInfoBold} display="block" boxSize={4} w={8} />
<Icon as={PiInfoBold} data-node-info-icon="true" display="block" boxSize={4} w={8} />
</Tooltip>
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ const InvocationNodeStatusIndicator = ({ nodeId }: Props) => {

return (
<Tooltip label={<TooltipLabel nodeExecutionState={nodeExecutionState} />} placement="top">
<Flex className={DRAG_HANDLE_CLASSNAME} w={5} h="full" alignItems="center" justifyContent="flex-end">
<Flex
className={DRAG_HANDLE_CLASSNAME}
data-node-status-indicator="true"
w={5}
h="full"
alignItems="center"
justifyContent="flex-end"
>
<StatusIcon nodeExecutionState={nodeExecutionState} />
</Flex>
</Tooltip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export const InputFieldTitle = memo((props: Props) => {
className={NO_FIT_ON_DOUBLE_CLICK_CLASS}
sx={labelSx}
noOfLines={1}
data-node-input-field-title="true"
data-is-invalid={isInvalid}
data-is-disabled={isDisabled}
data-is-added-to-form={isAddedToForm}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
import { useReactFlow } from '@xyflow/react';
import { logger } from 'app/logging/logger';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectWorkflowName } from 'features/nodes/store/selectors';
import {
selectShouldShowMinimapPanel,
shouldShowMinimapPanelChanged,
} from 'features/nodes/store/workflowSettingsSlice';
import { memo, useCallback } from 'react';
import { exportWorkflowAsPng } from 'features/nodes/util/workflowImageExport';
import { toast } from 'features/toast/toast';
import { memo, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiCameraBold,
PiFrameCornersBold,
PiMagnifyingGlassMinusBold,
PiMagnifyingGlassPlusBold,
Expand All @@ -16,11 +21,16 @@ import {

import { AutoLayoutPopover } from './AutoLayoutPopover';

const log = logger('workflows');

const ViewportControls = () => {
const { t } = useTranslation();
const { zoomIn, zoomOut, fitView } = useReactFlow();
const { zoomIn, zoomOut, fitView, getNodes, getNodesBounds } = useReactFlow();
const dispatch = useAppDispatch();
const shouldShowMinimapPanel = useAppSelector(selectShouldShowMinimapPanel);
const workflowName = useAppSelector(selectWorkflowName);
const fallbackWorkflowName = t('workflows.unnamedWorkflow');
const [isExportingWorkflow, setIsExportingWorkflow] = useState(false);

const handleClickedZoomIn = useCallback(() => {
zoomIn({ duration: 300 });
Expand All @@ -38,6 +48,54 @@ const ViewportControls = () => {
dispatch(shouldShowMinimapPanelChanged(!shouldShowMinimapPanel));
}, [shouldShowMinimapPanel, dispatch]);

const handleWorkflowImageExportError = useCallback(
(error?: unknown) => {
if (error) {
log.error({ error: error instanceof Error ? error.message : String(error) }, 'Workflow image export failed');
}
toast({
id: 'DOWNLOAD_WORKFLOW_IMAGE_ERROR',
status: 'error',
description: t('nodes.downloadWorkflowImageError'),
});
},
[t]
);

const handleClickedExportWorkflow = useCallback(() => {
if (isExportingWorkflow) {
return;
}

const flowElement = document.querySelector<HTMLElement>('#workflow-editor');
if (!flowElement) {
handleWorkflowImageExportError();
return;
}

setIsExportingWorkflow(true);
void new Promise<void>((resolve) => {
requestAnimationFrame(() => resolve());
})
.then(() =>
exportWorkflowAsPng({
flowElement,
bounds: getNodesBounds(getNodes()),
workflowName,
fallbackWorkflowName,
})
)
.catch(handleWorkflowImageExportError)
.finally(() => setIsExportingWorkflow(false));
}, [
fallbackWorkflowName,
getNodes,
getNodesBounds,
handleWorkflowImageExportError,
isExportingWorkflow,
workflowName,
]);

return (
<ButtonGroup orientation="vertical">
<IconButton
Expand All @@ -59,6 +117,14 @@ const ViewportControls = () => {
icon={<PiFrameCornersBold />}
/>
<AutoLayoutPopover />
<IconButton
tooltip={t('nodes.downloadWorkflowImage')}
aria-label={t('nodes.downloadWorkflowImage')}
isDisabled={isExportingWorkflow}
isLoading={isExportingWorkflow}
onClick={handleClickedExportWorkflow}
icon={<PiCameraBold />}
/>
<IconButton
tooltip={shouldShowMinimapPanel ? t('nodes.hideMinimapnodes') : t('nodes.showMinimapnodes')}
aria-label={shouldShowMinimapPanel ? t('nodes.hideMinimapnodes') : t('nodes.showMinimapnodes')}
Expand Down
5 changes: 5 additions & 0 deletions invokeai/frontend/web/src/features/nodes/types/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ export const HANDLE_TOOLTIP_OPEN_DELAY = 500;
*/
export const NODE_WIDTH = 320;

/**
* The grid spacing used by the workflow editor and workflow image export.
*/
export const WORKFLOW_GRID_SIZE = 25;

/**
* This class name is special - reactflow uses it to identify the drag handle of a node,
* applying the appropriate listeners to it.
Expand Down
Loading
Loading