From 77fa7519c4269aaf1ee1b9f0a22c0d24d5977219 Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Sat, 22 Apr 2023 20:13:35 +1200 Subject: [PATCH 1/7] chore(ui): Cleanup Invocation Component --- .../features/nodes/components/AddNodeMenu.tsx | 2 +- .../features/nodes/components/FieldHandle.tsx | 4 +- .../components/IAINode/IAINodeHeader.tsx | 37 ++++ .../components/IAINode/IAINodeInputs.tsx | 147 +++++++++++++ .../components/IAINode/IAINodeOutputs.tsx | 96 ++++++++ .../nodes/components/InvocationComponent.tsx | 206 +----------------- 6 files changed, 294 insertions(+), 198 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeInputs.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeOutputs.tsx diff --git a/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx b/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx index ee6db90ec1..9c8fc1cde9 100644 --- a/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx @@ -49,7 +49,7 @@ export const AddNodeMenu = () => { return ( } /> - + {map(invocationTemplates, ({ title, description, type }, key) => { return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx b/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx index cc5b430382..ad73059c09 100644 --- a/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx @@ -19,11 +19,11 @@ const handleBaseStyles: CSSProperties = { }; const inputHandleStyles: CSSProperties = { - left: '-1.7rem', + left: '-1.6rem', }; const outputHandleStyles: CSSProperties = { - right: '-1.7rem', + right: '-0.6rem', }; const requiredConnectionStyles: CSSProperties = { diff --git a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx new file mode 100644 index 0000000000..0fa985ac95 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx @@ -0,0 +1,37 @@ +import { Flex, Heading, Tooltip, Icon } from '@chakra-ui/react'; +import { InvocationTemplate } from 'features/nodes/types/types'; +import { MutableRefObject } from 'react'; +import { FaInfoCircle } from 'react-icons/fa'; + +interface IAINodeHeaderProps { + nodeId: string; + template: MutableRefObject; +} + +export default function IAINodeHeader(props: IAINodeHeaderProps) { + const { nodeId, template } = props; + return ( + + + + {template.current?.title} + + + + + + + ); +} diff --git a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeInputs.tsx b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeInputs.tsx new file mode 100644 index 0000000000..5f744fe989 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeInputs.tsx @@ -0,0 +1,147 @@ +import { + InputFieldTemplate, + InputFieldValue, + InvocationTemplate, +} from 'features/nodes/types/types'; +import { MutableRefObject, ReactNode } from 'react'; +import { map } from 'lodash'; +import { useAppSelector } from 'app/storeHooks'; +import { RootState } from 'app/store'; +import { + Box, + Flex, + FormControl, + FormLabel, + HStack, + Tooltip, + Icon, +} from '@chakra-ui/react'; +import { FieldHandle } from '../FieldHandle'; +import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection'; +import { InputFieldComponent } from '../InputFieldComponent'; +import { FaInfoCircle } from 'react-icons/fa'; + +interface IAINodeInputProps { + nodeId: string; + + input: InputFieldValue; + template?: InputFieldTemplate | undefined; + connected: boolean; +} + +function IAINodeInput(props: IAINodeInputProps) { + const { nodeId, input, template, connected } = props; + const isValidConnection = useIsValidConnection(); + + return ( + + + {!template ? ( + + Unknown input: {input.name} + + ) : ( + <> + + + {template?.title} + + + + + + + + {!['never', 'directOnly'].includes( + template?.inputRequirement ?? '' + ) && ( + + )} + + )} + + + ); +} + +interface IAINodeInputsProps { + nodeId: string; + template: MutableRefObject; + inputs: Record; +} + +export default function IAINodeInputs(props: IAINodeInputsProps) { + const { nodeId, template, inputs } = props; + + const connectedInputs = useAppSelector( + (state: RootState) => state.nodes.edges + ); + + const renderIAINodeInputs = () => { + const IAINodeInputsToRender: ReactNode[] = []; + const inputSockets = map(inputs); + + inputSockets.forEach((inputSocket) => { + const inputTemplate = template.current?.inputs[inputSocket.name]; + + const isConnected = Boolean( + connectedInputs.filter((connectedInput) => { + return ( + connectedInput.target === nodeId && + connectedInput.targetHandle === inputSocket.name + ); + }).length + ); + + IAINodeInputsToRender.push( + + ); + }); + + return ( + + {IAINodeInputsToRender} + + ); + }; + + return renderIAINodeInputs(); +} diff --git a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeOutputs.tsx b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeOutputs.tsx new file mode 100644 index 0000000000..64a07b38f3 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeOutputs.tsx @@ -0,0 +1,96 @@ +import { + InvocationTemplate, + OutputFieldTemplate, + OutputFieldValue, +} from 'features/nodes/types/types'; +import { MutableRefObject, ReactNode } from 'react'; +import { map } from 'lodash'; +import { useAppSelector } from 'app/storeHooks'; +import { RootState } from 'app/store'; +import { Box, Flex, FormControl, FormLabel, HStack } from '@chakra-ui/react'; +import { FieldHandle } from '../FieldHandle'; +import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection'; + +interface IAINodeOutputProps { + nodeId: string; + output: OutputFieldValue; + template?: OutputFieldTemplate | undefined; + connected: boolean; +} + +function IAINodeOutput(props: IAINodeOutputProps) { + const { nodeId, output, template, connected } = props; + const isValidConnection = useIsValidConnection(); + + return ( + + + {!template ? ( + + + Unknown Output: {output.name} + + + ) : ( + <> + + {template?.title} + + + + )} + + + ); +} + +interface IAINodeOutputsProps { + nodeId: string; + template: MutableRefObject; + outputs: Record; +} + +export default function IAINodeOutputs(props: IAINodeOutputsProps) { + const { nodeId, template, outputs } = props; + + const connectedInputs = useAppSelector( + (state: RootState) => state.nodes.edges + ); + + const renderIAINodeOutputs = () => { + const IAINodeOutputsToRender: ReactNode[] = []; + const outputSockets = map(outputs); + + outputSockets.forEach((outputSocket) => { + const outputTemplate = template.current?.outputs[outputSocket.name]; + + const isConnected = Boolean( + connectedInputs.filter((connectedInput) => { + return ( + connectedInput.source === nodeId && + connectedInput.sourceHandle === outputSocket.name + ); + }).length + ); + + IAINodeOutputsToRender.push( + + ); + }); + + return {IAINodeOutputsToRender}; + }; + + return renderIAINodeOutputs(); +} diff --git a/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx index 5f06ee9352..d4abdf0c96 100644 --- a/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx @@ -1,48 +1,18 @@ -import { NodeProps, useReactFlow } from 'reactflow'; -import { - Box, - Flex, - FormControl, - FormLabel, - Heading, - HStack, - Tooltip, - Icon, - Code, - Text, -} from '@chakra-ui/react'; -import { FaExclamationCircle, FaInfoCircle } from 'react-icons/fa'; +import { NodeProps } from 'reactflow'; +import { Box, Flex, Icon } from '@chakra-ui/react'; +import { FaExclamationCircle } from 'react-icons/fa'; import { InvocationValue } from '../types/types'; -import { InputFieldComponent } from './InputFieldComponent'; -import { FieldHandle } from './FieldHandle'; -import { isEqual, map, size } from 'lodash'; -import { memo, useMemo, useRef } from 'react'; -import { useIsValidConnection } from '../hooks/useIsValidConnection'; -import { createSelector } from '@reduxjs/toolkit'; -import { RootState } from 'app/store'; -import { useAppSelector } from 'app/storeHooks'; -import { useGetInvocationTemplate } from '../hooks/useInvocationTemplate'; -const connectedInputFieldsSelector = createSelector( - [(state: RootState) => state.nodes.edges], - (edges) => { - // return edges.map((e) => e.targetHandle); - return edges; - }, - { - memoizeOptions: { - resultEqualityCheck: isEqual, - }, - } -); +import { memo, useRef } from 'react'; +import { useGetInvocationTemplate } from '../hooks/useInvocationTemplate'; +import IAINodeOutputs from './IAINode/IAINodeOutputs'; +import IAINodeInputs from './IAINode/IAINodeInputs'; +import IAINodeHeader from './IAINode/IAINodeHeader'; export const InvocationComponent = memo((props: NodeProps) => { const { id: nodeId, data, selected } = props; const { type, inputs, outputs } = data; - const isValidConnection = useIsValidConnection(); - - const connectedInputs = useAppSelector(connectedInputFieldsSelector); const getInvocationTemplate = useGetInvocationTemplate(); // TODO: determine if a field/handle is connected and disable the input if so @@ -70,7 +40,6 @@ export const InvocationComponent = memo((props: NodeProps) => { return ( ) => { }} > - <> - {nodeId} - - - {template.current.title} - - - - - - {map(inputs, (input, i) => { - const { id: fieldId } = input; - const inputTemplate = template.current?.inputs[input.name]; - - if (!inputTemplate) { - return ( - - - - Unknown input: {input.name} - - - - ); - } - - const isConnected = Boolean( - connectedInputs.filter((connectedInput) => { - return ( - connectedInput.target === nodeId && - connectedInput.targetHandle === input.name - ); - }).length - ); - - return ( - - - - {inputTemplate?.title} - - - - - - - {!['never', 'directOnly'].includes( - inputTemplate?.inputRequirement ?? '' - ) && ( - - )} - - ); - })} - {map(outputs).map((output, i) => { - const outputTemplate = template.current?.outputs[output.name]; - - const isConnected = Boolean( - connectedInputs.filter((connectedInput) => { - return ( - connectedInput.source === nodeId && - connectedInput.sourceHandle === output.name - ); - }).length - ); - - if (!outputTemplate) { - return ( - - - - Unknown output: {output.name} - - - - ); - } - - return ( - - - - {outputTemplate?.title} Output - - - - - ); - })} - + + + - ); }); From ad41afe65e99a2cec61075aa948d90ebd98300bf Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Sat, 22 Apr 2023 21:25:01 +1200 Subject: [PATCH 2/7] feat(ui): Make Nodes Resizable --- .../components/IAINode/IAINodeResizer.tsx | 19 +++++++++++++++++++ .../nodes/components/InvocationComponent.tsx | 6 +++++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeResizer.tsx diff --git a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeResizer.tsx b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeResizer.tsx new file mode 100644 index 0000000000..fcce9ce1b7 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeResizer.tsx @@ -0,0 +1,19 @@ +import { NodeResizeControl, NodeResizerProps } from 'reactflow'; + +export default function IAINodeResizer(props: NodeResizerProps) { + const { ...rest } = props; + return ( + + ); +} diff --git a/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx index d4abdf0c96..d168e429f4 100644 --- a/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx @@ -1,4 +1,4 @@ -import { NodeProps } from 'reactflow'; +import { NodeProps, NodeResizeControl } from 'reactflow'; import { Box, Flex, Icon } from '@chakra-ui/react'; import { FaExclamationCircle } from 'react-icons/fa'; import { InvocationValue } from '../types/types'; @@ -8,6 +8,8 @@ import { useGetInvocationTemplate } from '../hooks/useInvocationTemplate'; import IAINodeOutputs from './IAINode/IAINodeOutputs'; import IAINodeInputs from './IAINode/IAINodeInputs'; import IAINodeHeader from './IAINode/IAINodeHeader'; +import { IoResize } from 'react-icons/io5'; +import IAINodeResizer from './IAINode/IAINodeResizer'; export const InvocationComponent = memo((props: NodeProps) => { const { id: nodeId, data, selected } = props; @@ -32,6 +34,7 @@ export const InvocationComponent = memo((props: NodeProps) => { > + ); @@ -51,6 +54,7 @@ export const InvocationComponent = memo((props: NodeProps) => { + ); From 94a07a8da7a2e0819cb2c2d1826742a0f0db1232 Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Sat, 22 Apr 2023 22:23:17 +1200 Subject: [PATCH 3/7] feat(ui): Make Nodes always spawn in center of work area --- .../src/features/nodes/hooks/useBuildInvocation.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useBuildInvocation.ts b/invokeai/frontend/web/src/features/nodes/hooks/useBuildInvocation.ts index 19eeac8378..cf8f9ec0f7 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useBuildInvocation.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useBuildInvocation.ts @@ -1,7 +1,7 @@ import { RootState } from 'app/store'; import { useAppSelector } from 'app/storeHooks'; import { reduce } from 'lodash'; -import { Node } from 'reactflow'; +import { Node, useReactFlow } from 'reactflow'; import { AnyInvocationType } from 'services/events/types'; import { v4 as uuidv4 } from 'uuid'; import { @@ -16,6 +16,8 @@ export const useBuildInvocation = () => { (state: RootState) => state.nodes.invocationTemplates ); + const reactflow = useReactFlow(); + return (type: AnyInvocationType) => { const template = invocationTemplates[type]; @@ -61,10 +63,15 @@ export const useBuildInvocation = () => { {} as Record ); + const { x, y } = reactflow.project({ + x: window.innerWidth / 2.5, + y: window.innerHeight / 8, + }); + const invocation: Node = { id: nodeId, type: 'invocation', - position: { x: 0, y: 0 }, + position: { x: x, y: y }, data: { id: nodeId, type, From 44a653925a038087b51ea787b8a5b6c38d3ed819 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 22 Apr 2023 20:29:42 +1000 Subject: [PATCH 4/7] feat(ui): node styling, controls - custom node controls - fix some types - fix badge colors via colorScheme - style nodes --- .../features/nodes/components/AddNodeMenu.tsx | 12 +++- .../features/nodes/components/FieldHandle.tsx | 4 +- .../src/features/nodes/components/Flow.tsx | 14 +++- .../components/IAINode/IAINodeHeader.tsx | 6 +- .../components/IAINode/IAINodeInputs.tsx | 17 ++--- .../components/IAINode/IAINodeResizer.tsx | 11 +-- .../nodes/components/InputFieldComponent.tsx | 2 +- .../nodes/components/InvocationComponent.tsx | 67 ++++++++++++------- .../features/nodes/components/NodeEditor.tsx | 21 ------ .../nodes/components/NodeGraphOverlay.tsx | 28 ++++++++ .../nodes/components/ViewportControls.tsx | 57 ++++++++++++++++ ...d.tsx.tsx => ArrayInputFieldComponent.tsx} | 2 +- .../fields/BooleanInputFieldComponent.tsx | 2 +- .../fields/EnumInputFieldComponent.tsx | 2 +- .../fields/ImageInputFieldComponent.tsx | 2 +- .../fields/LatentsInputFieldComponent.tsx | 2 +- .../fields/ModelInputFieldComponent.tsx | 2 +- .../fields/NumberInputFieldComponent.tsx | 2 +- .../fields/StringInputFieldComponent.tsx | 2 +- .../src/features/nodes/store/nodesSlice.ts | 6 ++ .../web/src/features/nodes/types/constants.ts | 9 +++ .../web/src/features/nodes/types/types.ts | 1 + invokeai/frontend/web/src/theme/theme.ts | 1 + 23 files changed, 193 insertions(+), 79 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/components/NodeGraphOverlay.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/ViewportControls.tsx rename invokeai/frontend/web/src/features/nodes/components/fields/{ArrayInputField.tsx.tsx => ArrayInputFieldComponent.tsx} (90%) diff --git a/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx b/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx index 9c8fc1cde9..1a4d625ccf 100644 --- a/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx @@ -10,7 +10,7 @@ import { MenuItem, IconButton, } from '@chakra-ui/react'; -import { FaPlus } from 'react-icons/fa'; +import { FaEllipsisV, FaPlus } from 'react-icons/fa'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { nodeAdded } from '../store/nodesSlice'; import { cloneDeep, map } from 'lodash'; @@ -18,6 +18,8 @@ import { RootState } from 'app/store'; import { useBuildInvocation } from '../hooks/useBuildInvocation'; import { addToast } from 'features/system/store/systemSlice'; import { makeToast } from 'features/system/hooks/useToastWatcher'; +import { IAIIconButton } from 'exports'; +import { AnyInvocationType } from 'services/events/types'; export const AddNodeMenu = () => { const dispatch = useAppDispatch(); @@ -29,7 +31,7 @@ export const AddNodeMenu = () => { const buildInvocation = useBuildInvocation(); const addNode = useCallback( - (nodeType: string) => { + (nodeType: AnyInvocationType) => { const invocation = buildInvocation(nodeType); if (!invocation) { @@ -48,7 +50,11 @@ export const AddNodeMenu = () => { return ( - } /> + } + /> {map(invocationTemplates, ({ title, description, type }, key) => { return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx b/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx index ad73059c09..b7291b24b1 100644 --- a/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx @@ -19,11 +19,11 @@ const handleBaseStyles: CSSProperties = { }; const inputHandleStyles: CSSProperties = { - left: '-1.6rem', + left: '-1rem', }; const outputHandleStyles: CSSProperties = { - right: '-0.6rem', + right: '-0.5rem', }; const requiredConnectionStyles: CSSProperties = { diff --git a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx index ad21e92b6d..41c9b016fd 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx @@ -20,12 +20,16 @@ import { edgesChanged, nodesChanged, } from '../store/nodesSlice'; -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; import { InvocationComponent } from './InvocationComponent'; import { AddNodeMenu } from './AddNodeMenu'; import { FieldTypeLegend } from './FieldTypeLegend'; import { Button } from '@chakra-ui/react'; import { nodesGraphBuilt } from 'services/thunks/session'; +import { IAIIconButton } from 'exports'; +import { InfoIcon } from '@chakra-ui/icons'; +import { ViewportControls } from './ViewportControls'; +import NodeGraphOverlay from './NodeGraphOverlay'; const nodeTypes = { invocation: InvocationComponent }; @@ -33,6 +37,9 @@ export const Flow = () => { const dispatch = useAppDispatch(); const nodes = useAppSelector((state: RootState) => state.nodes.nodes); const edges = useAppSelector((state: RootState) => state.nodes.edges); + const shouldShowGraphOverlay = useAppSelector( + (state: RootState) => state.nodes.shouldShowGraphOverlay + ); const onNodesChange: OnNodesChange = useCallback( (changes) => { @@ -95,9 +102,12 @@ export const Flow = () => { + {shouldShowGraphOverlay && } + + + - ); diff --git a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx index 0fa985ac95..c3130155fe 100644 --- a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx @@ -12,7 +12,7 @@ export default function IAINodeHeader(props: IAINodeHeaderProps) { const { nodeId, template } = props; return ( - + {template.current?.title} @@ -30,7 +30,7 @@ export default function IAINodeHeader(props: IAINodeHeaderProps) { hasArrow shouldWrapChildren > - + ); diff --git a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeInputs.tsx b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeInputs.tsx index 5f744fe989..293e0de698 100644 --- a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeInputs.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeInputs.tsx @@ -15,11 +15,13 @@ import { HStack, Tooltip, Icon, + Divider, } from '@chakra-ui/react'; import { FieldHandle } from '../FieldHandle'; import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection'; import { InputFieldComponent } from '../InputFieldComponent'; import { FaInfoCircle } from 'react-icons/fa'; +import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; interface IAINodeInputProps { nodeId: string; @@ -35,13 +37,8 @@ function IAINodeInput(props: IAINodeInputProps) { return ( - {template?.title} - + {template?.title} { + inputSockets.forEach((inputSocket, index) => { const inputTemplate = template.current?.inputs[inputSocket.name]; const isConnected = Boolean( @@ -126,6 +123,10 @@ export default function IAINodeInputs(props: IAINodeInputsProps) { }).length ); + if (index < inputSockets.length) { + IAINodeInputsToRender.push(); + } + IAINodeInputsToRender.push( { + const [nodeSelectedOutline, nodeShadow] = useToken('shadows', [ + 'nodeSelectedOutline', + 'dark-lg', + ]); + + return ( + + {props.children} + + ); +}; + export const InvocationComponent = memo((props: NodeProps) => { const { id: nodeId, data, selected } = props; const { type, inputs, outputs } = data; @@ -22,41 +47,31 @@ export const InvocationComponent = memo((props: NodeProps) => { if (!template.current) { return ( - + - + ); } return ( - - - + + + - - + + ); }); diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx index 98e3b2d19a..5739c3f60e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx @@ -3,15 +3,8 @@ import { Box } from '@chakra-ui/react'; import { ReactFlowProvider } from 'reactflow'; import { Flow } from './Flow'; -import { useAppSelector } from 'app/storeHooks'; -import { RootState } from 'app/store'; -import { buildNodesGraph } from '../util/nodesGraphBuilder/buildNodesGraph'; const NodeEditor = () => { - const state = useAppSelector((state: RootState) => state); - - const graph = buildNodesGraph(state); - return ( { - - {JSON.stringify(graph, null, 2)} - ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeGraphOverlay.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeGraphOverlay.tsx new file mode 100644 index 0000000000..80bc45bc5b --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/NodeGraphOverlay.tsx @@ -0,0 +1,28 @@ +import { Box } from '@chakra-ui/react'; +import { RootState } from 'app/store'; +import { useAppSelector } from 'app/storeHooks'; +import { buildNodesGraph } from '../util/nodesGraphBuilder/buildNodesGraph'; + +export default function NodeGraphOverlay() { + const state = useAppSelector((state: RootState) => state); + const graph = buildNodesGraph(state); + + return ( + + {JSON.stringify(graph, null, 2)} + + ); +} diff --git a/invokeai/frontend/web/src/features/nodes/components/ViewportControls.tsx b/invokeai/frontend/web/src/features/nodes/components/ViewportControls.tsx new file mode 100644 index 0000000000..c43c6c77de --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/ViewportControls.tsx @@ -0,0 +1,57 @@ +import { ButtonGroup } from '@chakra-ui/react'; +import { useAppDispatch, useAppSelector } from 'app/storeHooks'; +import { IAIIconButton } from 'exports'; +import { useCallback } from 'react'; +import { FaCode, FaExpand, FaMinus, FaPlus } from 'react-icons/fa'; +import { useReactFlow } from 'reactflow'; +import { shouldShowGraphOverlayChanged } from '../store/nodesSlice'; + +export const ViewportControls = () => { + const { zoomIn, zoomOut, fitView } = useReactFlow(); + const dispatch = useAppDispatch(); + const shouldShowGraphOverlay = useAppSelector( + (state) => state.nodes.shouldShowGraphOverlay + ); + + const handleClickedZoomIn = useCallback(() => { + zoomIn(); + }, [zoomIn]); + + const handleClickedZoomOut = useCallback(() => { + zoomOut(); + }, [zoomOut]); + + const handleClickedFitView = useCallback(() => { + fitView(); + }, [fitView]); + + const handleClickedToggleGraphOverlay = useCallback(() => { + dispatch(shouldShowGraphOverlayChanged(!shouldShowGraphOverlay)); + }, [shouldShowGraphOverlay, dispatch]); + + return ( + + } + /> + } + /> + } + /> + } + /> + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputField.tsx.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputFieldComponent.tsx similarity index 90% rename from invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputField.tsx.tsx rename to invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputFieldComponent.tsx index d9717f14a4..1dd4b1d8af 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputField.tsx.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputFieldComponent.tsx @@ -1,7 +1,7 @@ import { ArrayInputFieldTemplate, ArrayInputFieldValue, -} from 'features/nodes/types'; +} from 'features/nodes/types/types'; import { FaImage, FaList } from 'react-icons/fa'; import { FieldComponentProps } from './types'; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/BooleanInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/BooleanInputFieldComponent.tsx index f9fe404f82..948aa2bb0a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/BooleanInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/BooleanInputFieldComponent.tsx @@ -4,7 +4,7 @@ import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; import { BooleanInputFieldTemplate, BooleanInputFieldValue, -} from 'features/nodes/types'; +} from 'features/nodes/types/types'; import { ChangeEvent } from 'react'; import { FieldComponentProps } from './types'; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/EnumInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/EnumInputFieldComponent.tsx index 8de8e17484..87425a28f2 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/EnumInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/EnumInputFieldComponent.tsx @@ -4,7 +4,7 @@ import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; import { EnumInputFieldTemplate, EnumInputFieldValue, -} from 'features/nodes/types'; +} from 'features/nodes/types/types'; import { ChangeEvent } from 'react'; import { FieldComponentProps } from './types'; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx index 599fa61e38..d07dfa4c6c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx @@ -8,7 +8,7 @@ import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; import { ImageInputFieldTemplate, ImageInputFieldValue, -} from 'features/nodes/types'; +} from 'features/nodes/types/types'; import { DragEvent, useCallback, useState } from 'react'; import { FaImage } from 'react-icons/fa'; import { ImageType } from 'services/api'; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/LatentsInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/LatentsInputFieldComponent.tsx index ed053aab7c..1edf07e014 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/LatentsInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/LatentsInputFieldComponent.tsx @@ -1,7 +1,7 @@ import { LatentsInputFieldTemplate, LatentsInputFieldValue, -} from 'features/nodes/types'; +} from 'features/nodes/types/types'; import { FieldComponentProps } from './types'; export const LatentsInputFieldComponent = ( diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx index 5aaf83a186..b4f60acbd0 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx @@ -6,7 +6,7 @@ import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; import { ModelInputFieldTemplate, ModelInputFieldValue, -} from 'features/nodes/types'; +} from 'features/nodes/types/types'; import { selectModelsById, selectModelsIds, diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/NumberInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/NumberInputFieldComponent.tsx index 57b8527e00..3ef54a8a5a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/NumberInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/NumberInputFieldComponent.tsx @@ -12,7 +12,7 @@ import { FloatInputFieldValue, IntegerInputFieldTemplate, IntegerInputFieldValue, -} from 'features/nodes/types'; +} from 'features/nodes/types/types'; import { FieldComponentProps } from './types'; export const NumberInputFieldComponent = ( diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/StringInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/StringInputFieldComponent.tsx index 7ed3b5d435..54341df31a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/StringInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/StringInputFieldComponent.tsx @@ -4,7 +4,7 @@ import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; import { StringInputFieldTemplate, StringInputFieldValue, -} from 'features/nodes/types'; +} from 'features/nodes/types/types'; import { ChangeEvent } from 'react'; import { FieldComponentProps } from './types'; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 1ce806de57..f4c4f9522d 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -24,6 +24,7 @@ export type NodesState = { invocationTemplates: Record; connectionStartParams: OnConnectStartParams | null; lastGraph: Graph | null; + shouldShowGraphOverlay: boolean; }; export const initialNodesState: NodesState = { @@ -33,6 +34,7 @@ export const initialNodesState: NodesState = { invocationTemplates: {}, connectionStartParams: null, lastGraph: null, + shouldShowGraphOverlay: false, }; const nodesSlice = createSlice({ @@ -77,6 +79,9 @@ const nodesSlice = createSlice({ state.nodes[nodeIndex].data.inputs[fieldName].value = value; } }, + shouldShowGraphOverlayChanged: (state, action: PayloadAction) => { + state.shouldShowGraphOverlay = action.payload; + }, }, extraReducers(builder) { builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => { @@ -98,6 +103,7 @@ export const { connectionMade, connectionStarted, connectionEnded, + shouldShowGraphOverlayChanged, } = nodesSlice.actions; export default nodesSlice.reducer; diff --git a/invokeai/frontend/web/src/features/nodes/types/constants.ts b/invokeai/frontend/web/src/features/nodes/types/constants.ts index 0fb8acf5bf..01497651e3 100644 --- a/invokeai/frontend/web/src/features/nodes/types/constants.ts +++ b/invokeai/frontend/web/src/features/nodes/types/constants.ts @@ -22,46 +22,55 @@ const getColorTokenCssVariable = (color: string) => export const FIELDS: Record = { integer: { + color: 'red', colorCssVar: getColorTokenCssVariable('red'), title: 'Integer', description: 'Integers are whole numbers, without a decimal point.', }, float: { + color: 'orange', colorCssVar: getColorTokenCssVariable('orange'), title: 'Float', description: 'Floats are numbers with a decimal point.', }, string: { + color: 'yellow', colorCssVar: getColorTokenCssVariable('yellow'), title: 'String', description: 'Strings are text.', }, boolean: { + color: 'green', colorCssVar: getColorTokenCssVariable('green'), title: 'Boolean', description: 'Booleans are true or false.', }, enum: { + color: 'blue', colorCssVar: getColorTokenCssVariable('blue'), title: 'Enum', description: 'Enums are values that may be one of a number of options.', }, image: { + color: 'purple', colorCssVar: getColorTokenCssVariable('purple'), title: 'Image', description: 'Images may be passed between nodes.', }, latents: { + color: 'pink', colorCssVar: getColorTokenCssVariable('pink'), title: 'Latents', description: 'Latents may be passed between nodes.', }, model: { + color: 'teal', colorCssVar: getColorTokenCssVariable('teal'), title: 'Model', description: 'Models are models.', }, array: { + color: 'gray', colorCssVar: getColorTokenCssVariable('gray'), title: 'Array', description: 'TODO: Array type description.', diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts index 1f35712d39..4b5548e351 100644 --- a/invokeai/frontend/web/src/features/nodes/types/types.ts +++ b/invokeai/frontend/web/src/features/nodes/types/types.ts @@ -39,6 +39,7 @@ export type InvocationTemplate = { }; export type FieldUIConfig = { + color: string; colorCssVar: string; title: string; description: string; diff --git a/invokeai/frontend/web/src/theme/theme.ts b/invokeai/frontend/web/src/theme/theme.ts index 1753bae77b..442d76f12f 100644 --- a/invokeai/frontend/web/src/theme/theme.ts +++ b/invokeai/frontend/web/src/theme/theme.ts @@ -53,6 +53,7 @@ export const theme: ThemeOverride = { working: `0 0 7px var(--invokeai-colors-working-400)`, error: `0 0 7px var(--invokeai-colors-error-400)`, }, + nodeSelectedOutline: `0 0 0 2px var(--invokeai-colors-base-500)`, }, colors: { ...invokeAIThemeColors, From 4901911c1ab078583c0b3c908df36e29119645c7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 22 Apr 2023 21:51:25 +1000 Subject: [PATCH 5/7] fix(ui): improve nodes performance --- .../features/nodes/components/AddNodeMenu.tsx | 2 +- .../features/nodes/components/FieldHandle.tsx | 9 +- .../src/features/nodes/components/Flow.tsx | 28 ++--- .../components/IAINode/IAINodeHeader.tsx | 6 +- .../components/IAINode/IAINodeInputs.tsx | 16 +-- .../components/IAINode/IAINodeOutputs.tsx | 17 +-- .../nodes/components/InvocationComponent.tsx | 35 +++++- .../components/panels/BottomLeftPanel.tsx.tsx | 11 ++ .../components/panels/TopCenterPanel.tsx | 23 ++++ .../nodes/components/panels/TopLeftPanel.tsx | 11 ++ .../nodes/components/panels/TopRightPanel.tsx | 21 ++++ .../nodes/hooks/useBuildInvocation.ts | 117 ++++++++++-------- .../nodes/hooks/useInvocationTemplate.ts | 16 --- 13 files changed, 195 insertions(+), 117 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/components/panels/BottomLeftPanel.tsx.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/panels/TopLeftPanel.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/panels/TopRightPanel.tsx delete mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useInvocationTemplate.ts diff --git a/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx b/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx index 1a4d625ccf..713ab160e7 100644 --- a/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx @@ -49,7 +49,7 @@ export const AddNodeMenu = () => { ); return ( - + { +const FieldHandle = (props: FieldHandleProps) => { const { nodeId, field, isValidConnection, handleType, styles } = props; const { name, title, type, description } = field; + console.log(props); + return ( { ); }; + +export default memo(FieldHandle); diff --git a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx index 41c9b016fd..fd89244bc8 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx @@ -30,6 +30,10 @@ import { IAIIconButton } from 'exports'; import { InfoIcon } from '@chakra-ui/icons'; import { ViewportControls } from './ViewportControls'; import NodeGraphOverlay from './NodeGraphOverlay'; +import TopLeftPanel from './panels/TopLeftPanel'; +import TopRightPanel from './panels/TopRightPanel'; +import TopCenterPanel from './panels/TopCenterPanel'; +import BottomLeftPanel from './panels/BottomLeftPanel.tsx'; const nodeTypes = { invocation: InvocationComponent }; @@ -37,9 +41,6 @@ export const Flow = () => { const dispatch = useAppDispatch(); const nodes = useAppSelector((state: RootState) => state.nodes.nodes); const edges = useAppSelector((state: RootState) => state.nodes.edges); - const shouldShowGraphOverlay = useAppSelector( - (state: RootState) => state.nodes.shouldShowGraphOverlay - ); const onNodesChange: OnNodesChange = useCallback( (changes) => { @@ -76,10 +77,6 @@ export const Flow = () => { [dispatch] ); - const handleInvoke = useCallback(() => { - dispatch(nodesGraphBuilt()); - }, [dispatch]); - return ( { style: { strokeWidth: 2 }, }} > - - - - - - - - - {shouldShowGraphOverlay && } - - - - + + + + diff --git a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx index c3130155fe..4a5ded7734 100644 --- a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx @@ -5,7 +5,7 @@ import { FaInfoCircle } from 'react-icons/fa'; interface IAINodeHeaderProps { nodeId: string; - template: MutableRefObject; + template: InvocationTemplate; } export default function IAINodeHeader(props: IAINodeHeaderProps) { @@ -21,11 +21,11 @@ export default function IAINodeHeader(props: IAINodeHeaderProps) { > - {template.current?.title} + {template.title} ; + template: InvocationTemplate; inputs: Record; } -export default function IAINodeInputs(props: IAINodeInputsProps) { +const IAINodeInputs = (props: IAINodeInputsProps) => { const { nodeId, template, inputs } = props; const connectedInputs = useAppSelector( @@ -112,7 +111,7 @@ export default function IAINodeInputs(props: IAINodeInputsProps) { const inputSockets = map(inputs); inputSockets.forEach((inputSocket, index) => { - const inputTemplate = template.current?.inputs[inputSocket.name]; + const inputTemplate = template.inputs[inputSocket.name]; const isConnected = Boolean( connectedInputs.filter((connectedInput) => { @@ -129,6 +128,7 @@ export default function IAINodeInputs(props: IAINodeInputsProps) { IAINodeInputsToRender.push( + {!template ? ( @@ -52,11 +52,11 @@ function IAINodeOutput(props: IAINodeOutputProps) { interface IAINodeOutputsProps { nodeId: string; - template: MutableRefObject; + template: InvocationTemplate; outputs: Record; } -export default function IAINodeOutputs(props: IAINodeOutputsProps) { +const IAINodeOutputs = (props: IAINodeOutputsProps) => { const { nodeId, template, outputs } = props; const connectedInputs = useAppSelector( @@ -68,7 +68,7 @@ export default function IAINodeOutputs(props: IAINodeOutputsProps) { const outputSockets = map(outputs); outputSockets.forEach((outputSocket) => { - const outputTemplate = template.current?.outputs[outputSocket.name]; + const outputTemplate = template.outputs[outputSocket.name]; const isConnected = Boolean( connectedInputs.filter((connectedInput) => { @@ -81,6 +81,7 @@ export default function IAINodeOutputs(props: IAINodeOutputsProps) { IAINodeOutputsToRender.push( { ); }; +const makeTemplateSelector = (type: AnyInvocationType) => + createSelector( + [(state: RootState) => state.nodes], + (nodes) => { + const template = nodes.invocationTemplates[type]; + if (!template) { + return; + } + return template; + }, + { + memoizeOptions: { + resultEqualityCheck: ( + a: InvocationTemplate | undefined, + b: InvocationTemplate | undefined + ) => a !== undefined && b !== undefined && a.type === b.type, + }, + } + ); + export const InvocationComponent = memo((props: NodeProps) => { const { id: nodeId, data, selected } = props; const { type, inputs, outputs } = data; - const getInvocationTemplate = useGetInvocationTemplate(); - // TODO: determine if a field/handle is connected and disable the input if so + const templateSelector = useMemo(() => makeTemplateSelector(type), [type]); - const template = useRef(getInvocationTemplate(type)); + const template = useAppSelector(templateSelector); - if (!template.current) { + if (!template) { return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/panels/BottomLeftPanel.tsx.tsx b/invokeai/frontend/web/src/features/nodes/components/panels/BottomLeftPanel.tsx.tsx new file mode 100644 index 0000000000..7957acf271 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/panels/BottomLeftPanel.tsx.tsx @@ -0,0 +1,11 @@ +import { memo } from 'react'; +import { Panel } from 'reactflow'; +import { ViewportControls } from '../ViewportControls'; + +const BottomLeftPanel = () => ( + + + +); + +export default memo(BottomLeftPanel); diff --git a/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx new file mode 100644 index 0000000000..dfedb284ec --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx @@ -0,0 +1,23 @@ +import { useAppDispatch } from 'app/storeHooks'; +import IAIButton from 'common/components/IAIButton'; +import { memo, useCallback } from 'react'; +import { Panel } from 'reactflow'; +import { nodesGraphBuilt } from 'services/thunks/session'; + +const TopCenterPanel = () => { + const dispatch = useAppDispatch(); + + const handleInvoke = useCallback(() => { + dispatch(nodesGraphBuilt()); + }, [dispatch]); + + return ( + + + Will it blend? + + + ); +}; + +export default memo(TopCenterPanel); diff --git a/invokeai/frontend/web/src/features/nodes/components/panels/TopLeftPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/panels/TopLeftPanel.tsx new file mode 100644 index 0000000000..29e7cc1b14 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/panels/TopLeftPanel.tsx @@ -0,0 +1,11 @@ +import { memo } from 'react'; +import { Panel } from 'reactflow'; +import { AddNodeMenu } from '../AddNodeMenu'; + +const TopLeftPanel = () => ( + + + +); + +export default memo(TopLeftPanel); diff --git a/invokeai/frontend/web/src/features/nodes/components/panels/TopRightPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/panels/TopRightPanel.tsx new file mode 100644 index 0000000000..094b7bb797 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/panels/TopRightPanel.tsx @@ -0,0 +1,21 @@ +import { RootState } from 'app/store'; +import { useAppSelector } from 'app/storeHooks'; +import { memo } from 'react'; +import { Panel } from 'reactflow'; +import { FieldTypeLegend } from '../FieldTypeLegend'; +import NodeGraphOverlay from '../NodeGraphOverlay'; + +const TopRightPanel = () => { + const shouldShowGraphOverlay = useAppSelector( + (state: RootState) => state.nodes.shouldShowGraphOverlay + ); + + return ( + + + {shouldShowGraphOverlay && } + + ); +}; + +export default memo(TopRightPanel); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useBuildInvocation.ts b/invokeai/frontend/web/src/features/nodes/hooks/useBuildInvocation.ts index cf8f9ec0f7..a5422c3e95 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useBuildInvocation.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useBuildInvocation.ts @@ -1,6 +1,8 @@ +import { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'app/store'; import { useAppSelector } from 'app/storeHooks'; import { reduce } from 'lodash'; +import { useCallback } from 'react'; import { Node, useReactFlow } from 'reactflow'; import { AnyInvocationType } from 'services/events/types'; import { v4 as uuidv4 } from 'uuid'; @@ -11,75 +13,82 @@ import { } from '../types/types'; import { buildInputFieldValue } from '../util/fieldValueBuilders'; +const templatesSelector = createSelector( + [(state: RootState) => state.nodes], + (nodes) => nodes.invocationTemplates, + { memoizeOptions: { resultEqualityCheck: (a, b) => true } } +); + export const useBuildInvocation = () => { - const invocationTemplates = useAppSelector( - (state: RootState) => state.nodes.invocationTemplates - ); + const invocationTemplates = useAppSelector(templatesSelector); - const reactflow = useReactFlow(); + const flow = useReactFlow(); - return (type: AnyInvocationType) => { - const template = invocationTemplates[type]; + return useCallback( + (type: AnyInvocationType) => { + const template = invocationTemplates[type]; - if (template === undefined) { - console.error(`Unable to find template ${type}.`); - return; - } + if (template === undefined) { + console.error(`Unable to find template ${type}.`); + return; + } - const nodeId = uuidv4(); + const nodeId = uuidv4(); - const inputs = reduce( - template.inputs, - (inputsAccumulator, inputTemplate, inputName) => { - const fieldId = uuidv4(); + const inputs = reduce( + template.inputs, + (inputsAccumulator, inputTemplate, inputName) => { + const fieldId = uuidv4(); - const inputFieldValue: InputFieldValue = buildInputFieldValue( - fieldId, - inputTemplate - ); + const inputFieldValue: InputFieldValue = buildInputFieldValue( + fieldId, + inputTemplate + ); - inputsAccumulator[inputName] = inputFieldValue; + inputsAccumulator[inputName] = inputFieldValue; - return inputsAccumulator; - }, - {} as Record - ); + return inputsAccumulator; + }, + {} as Record + ); - const outputs = reduce( - template.outputs, - (outputsAccumulator, outputTemplate, outputName) => { - const fieldId = uuidv4(); + const outputs = reduce( + template.outputs, + (outputsAccumulator, outputTemplate, outputName) => { + const fieldId = uuidv4(); - const outputFieldValue: OutputFieldValue = { - id: fieldId, - name: outputName, - type: outputTemplate.type, - }; + const outputFieldValue: OutputFieldValue = { + id: fieldId, + name: outputName, + type: outputTemplate.type, + }; - outputsAccumulator[outputName] = outputFieldValue; + outputsAccumulator[outputName] = outputFieldValue; - return outputsAccumulator; - }, - {} as Record - ); + return outputsAccumulator; + }, + {} as Record + ); - const { x, y } = reactflow.project({ - x: window.innerWidth / 2.5, - y: window.innerHeight / 8, - }); + const { x, y } = flow.project({ + x: window.innerWidth / 2.5, + y: window.innerHeight / 8, + }); - const invocation: Node = { - id: nodeId, - type: 'invocation', - position: { x: x, y: y }, - data: { + const invocation: Node = { id: nodeId, - type, - inputs, - outputs, - }, - }; + type: 'invocation', + position: { x: x, y: y }, + data: { + id: nodeId, + type, + inputs, + outputs, + }, + }; - return invocation; - }; + return invocation; + }, + [invocationTemplates, flow] + ); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInvocationTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInvocationTemplate.ts deleted file mode 100644 index f58e82b897..0000000000 --- a/invokeai/frontend/web/src/features/nodes/hooks/useInvocationTemplate.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useAppSelector } from 'app/storeHooks'; -import { invocationTemplatesSelector } from '../store/selectors/invocationTemplatesSelector'; - -export const useGetInvocationTemplate = () => { - const invocationTemplates = useAppSelector(invocationTemplatesSelector); - - return (invocationType: string) => { - const template = invocationTemplates[invocationType]; - - if (!template) { - return; - } - - return template; - }; -}; From 43addc15484baf6f93a4c17d65af5bd6513f1f74 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 22 Apr 2023 22:03:27 +1000 Subject: [PATCH 6/7] fix(ui): memoize everything nodes --- .../features/nodes/components/AddNodeMenu.tsx | 6 ++++-- .../nodes/components/FieldTypeLegend.tsx | 5 ++++- .../src/features/nodes/components/Flow.tsx | 13 +----------- .../components/IAINode/IAINodeHeader.tsx | 8 ++++--- .../components/IAINode/IAINodeInputs.tsx | 2 +- .../components/IAINode/IAINodeResizer.tsx | 7 +++++-- .../nodes/components/InputFieldComponent.tsx | 21 +++++++++++-------- .../nodes/components/InvocationComponent.tsx | 6 ++---- .../features/nodes/components/NodeEditor.tsx | 3 ++- .../nodes/components/NodeGraphOverlay.tsx | 8 ++++--- .../nodes/components/ViewportControls.tsx | 6 ++++-- .../fields/ArrayInputFieldComponent.tsx | 7 +++++-- .../fields/BooleanInputFieldComponent.tsx | 6 ++++-- .../fields/EnumInputFieldComponent.tsx | 6 ++++-- .../fields/ImageInputFieldComponent.tsx | 6 ++++-- .../fields/LatentsInputFieldComponent.tsx | 5 ++++- .../fields/ModelInputFieldComponent.tsx | 6 ++++-- .../fields/NumberInputFieldComponent.tsx | 5 ++++- .../fields/StringInputFieldComponent.tsx | 6 ++++-- .../components/panels/BottomLeftPanel.tsx.tsx | 2 +- .../nodes/components/panels/TopLeftPanel.tsx | 2 +- .../nodes/components/panels/TopRightPanel.tsx | 2 +- 22 files changed, 81 insertions(+), 57 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx b/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx index 713ab160e7..5acd0c0530 100644 --- a/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx @@ -1,7 +1,7 @@ import { v4 as uuidv4 } from 'uuid'; import 'reactflow/dist/style.css'; -import { useCallback } from 'react'; +import { memo, useCallback } from 'react'; import { Tooltip, Menu, @@ -21,7 +21,7 @@ import { makeToast } from 'features/system/hooks/useToastWatcher'; import { IAIIconButton } from 'exports'; import { AnyInvocationType } from 'services/events/types'; -export const AddNodeMenu = () => { +const AddNodeMenu = () => { const dispatch = useAppDispatch(); const invocationTemplates = useAppSelector( @@ -67,3 +67,5 @@ export const AddNodeMenu = () => { ); }; + +export default memo(AddNodeMenu); diff --git a/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx b/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx index a420376016..4f46b03b7a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx @@ -2,8 +2,9 @@ import 'reactflow/dist/style.css'; import { Tooltip, Badge, HStack } from '@chakra-ui/react'; import { map } from 'lodash'; import { FIELDS } from '../types/constants'; +import { memo } from 'react'; -export const FieldTypeLegend = () => { +const FieldTypeLegend = () => { return ( {map(FIELDS, ({ title, description, color }, key) => ( @@ -16,3 +17,5 @@ export const FieldTypeLegend = () => { ); }; + +export default memo(FieldTypeLegend); diff --git a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx index fd89244bc8..173458d285 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx @@ -1,15 +1,12 @@ import { Background, - Controls, MiniMap, OnConnect, OnEdgesChange, OnNodesChange, ReactFlow, - ConnectionLineType, OnConnectStart, OnConnectEnd, - Panel, } from 'reactflow'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { RootState } from 'app/store'; @@ -20,16 +17,8 @@ import { edgesChanged, nodesChanged, } from '../store/nodesSlice'; -import { useCallback, useState } from 'react'; +import { useCallback } from 'react'; import { InvocationComponent } from './InvocationComponent'; -import { AddNodeMenu } from './AddNodeMenu'; -import { FieldTypeLegend } from './FieldTypeLegend'; -import { Button } from '@chakra-ui/react'; -import { nodesGraphBuilt } from 'services/thunks/session'; -import { IAIIconButton } from 'exports'; -import { InfoIcon } from '@chakra-ui/icons'; -import { ViewportControls } from './ViewportControls'; -import NodeGraphOverlay from './NodeGraphOverlay'; import TopLeftPanel from './panels/TopLeftPanel'; import TopRightPanel from './panels/TopRightPanel'; import TopCenterPanel from './panels/TopCenterPanel'; diff --git a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx index 4a5ded7734..2a61d4cc2b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx @@ -1,6 +1,6 @@ import { Flex, Heading, Tooltip, Icon } from '@chakra-ui/react'; import { InvocationTemplate } from 'features/nodes/types/types'; -import { MutableRefObject } from 'react'; +import { memo, MutableRefObject } from 'react'; import { FaInfoCircle } from 'react-icons/fa'; interface IAINodeHeaderProps { @@ -8,7 +8,7 @@ interface IAINodeHeaderProps { template: InvocationTemplate; } -export default function IAINodeHeader(props: IAINodeHeaderProps) { +const IAINodeHeader = (props: IAINodeHeaderProps) => { const { nodeId, template } = props; return ( ); -} +}; + +export default memo(IAINodeHeader); diff --git a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeInputs.tsx b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeInputs.tsx index 6422ab4555..61cdf67803 100644 --- a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeInputs.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeInputs.tsx @@ -19,7 +19,7 @@ import { } from '@chakra-ui/react'; import FieldHandle from '../FieldHandle'; import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection'; -import { InputFieldComponent } from '../InputFieldComponent'; +import InputFieldComponent from '../InputFieldComponent'; import { FaInfoCircle } from 'react-icons/fa'; import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; diff --git a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeResizer.tsx b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeResizer.tsx index 2ef82f6a9b..0fa13c82b6 100644 --- a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeResizer.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeResizer.tsx @@ -1,6 +1,7 @@ +import { memo } from 'react'; import { NodeResizeControl, NodeResizerProps } from 'reactflow'; -export default function IAINodeResizer(props: NodeResizerProps) { +const IAINodeResizer = (props: NodeResizerProps) => { const { ...rest } = props; return ( ); -} +}; + +export default memo(IAINodeResizer); diff --git a/invokeai/frontend/web/src/features/nodes/components/InputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/InputFieldComponent.tsx index 80a3241e13..21e4b9fcfb 100644 --- a/invokeai/frontend/web/src/features/nodes/components/InputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/InputFieldComponent.tsx @@ -1,13 +1,14 @@ import { Box } from '@chakra-ui/react'; +import { memo } from 'react'; import { InputFieldTemplate, InputFieldValue } from '../types/types'; -import { ArrayInputFieldComponent } from './fields/ArrayInputFieldComponent'; -import { BooleanInputFieldComponent } from './fields/BooleanInputFieldComponent'; -import { EnumInputFieldComponent } from './fields/EnumInputFieldComponent'; -import { ImageInputFieldComponent } from './fields/ImageInputFieldComponent'; -import { LatentsInputFieldComponent } from './fields/LatentsInputFieldComponent'; -import { ModelInputFieldComponent } from './fields/ModelInputFieldComponent'; -import { NumberInputFieldComponent } from './fields/NumberInputFieldComponent'; -import { StringInputFieldComponent } from './fields/StringInputFieldComponent'; +import ArrayInputFieldComponent from './fields/ArrayInputFieldComponent'; +import BooleanInputFieldComponent from './fields/BooleanInputFieldComponent'; +import EnumInputFieldComponent from './fields/EnumInputFieldComponent'; +import ImageInputFieldComponent from './fields/ImageInputFieldComponent'; +import LatentsInputFieldComponent from './fields/LatentsInputFieldComponent'; +import ModelInputFieldComponent from './fields/ModelInputFieldComponent'; +import NumberInputFieldComponent from './fields/NumberInputFieldComponent'; +import StringInputFieldComponent from './fields/StringInputFieldComponent'; type InputFieldComponentProps = { nodeId: string; @@ -16,7 +17,7 @@ type InputFieldComponentProps = { }; // build an individual input element based on the schema -export const InputFieldComponent = (props: InputFieldComponentProps) => { +const InputFieldComponent = (props: InputFieldComponentProps) => { const { nodeId, field, template } = props; const { type, value } = field; @@ -105,3 +106,5 @@ export const InputFieldComponent = (props: InputFieldComponentProps) => { return Unknown field type: {type}; }; + +export default memo(InputFieldComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx index f85208c936..c0ddf1c3b3 100644 --- a/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx @@ -1,14 +1,12 @@ -import { NodeProps, NodeResizeControl } from 'reactflow'; +import { NodeProps } from 'reactflow'; import { Box, Flex, Icon, useToken } from '@chakra-ui/react'; import { FaExclamationCircle } from 'react-icons/fa'; import { InvocationTemplate, InvocationValue } from '../types/types'; -import { memo, PropsWithChildren, useMemo, useRef } from 'react'; -import { useGetInvocationTemplate } from '../hooks/useInvocationTemplate'; +import { memo, PropsWithChildren, useMemo } from 'react'; import IAINodeOutputs from './IAINode/IAINodeOutputs'; import IAINodeInputs from './IAINode/IAINodeInputs'; import IAINodeHeader from './IAINode/IAINodeHeader'; -import { IoResize } from 'react-icons/io5'; import IAINodeResizer from './IAINode/IAINodeResizer'; import { RootState } from 'app/store'; import { AnyInvocationType } from 'services/events/types'; diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx index 5739c3f60e..05f8c2a893 100644 --- a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx @@ -3,6 +3,7 @@ import { Box } from '@chakra-ui/react'; import { ReactFlowProvider } from 'reactflow'; import { Flow } from './Flow'; +import { memo } from 'react'; const NodeEditor = () => { return ( @@ -22,4 +23,4 @@ const NodeEditor = () => { ); }; -export default NodeEditor; +export default memo(NodeEditor); diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeGraphOverlay.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeGraphOverlay.tsx index 80bc45bc5b..88a125e542 100644 --- a/invokeai/frontend/web/src/features/nodes/components/NodeGraphOverlay.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/NodeGraphOverlay.tsx @@ -1,9 +1,10 @@ import { Box } from '@chakra-ui/react'; import { RootState } from 'app/store'; import { useAppSelector } from 'app/storeHooks'; +import { memo } from 'react'; import { buildNodesGraph } from '../util/nodesGraphBuilder/buildNodesGraph'; -export default function NodeGraphOverlay() { +const NodeGraphOverlay = () => { const state = useAppSelector((state: RootState) => state); const graph = buildNodesGraph(state); @@ -14,7 +15,6 @@ export default function NodeGraphOverlay() { position="absolute" top={10} right={2} - userSelect="none" opacity={0.7} background="base.800" p={2} @@ -25,4 +25,6 @@ export default function NodeGraphOverlay() { {JSON.stringify(graph, null, 2)} ); -} +}; + +export default memo(NodeGraphOverlay); diff --git a/invokeai/frontend/web/src/features/nodes/components/ViewportControls.tsx b/invokeai/frontend/web/src/features/nodes/components/ViewportControls.tsx index c43c6c77de..249e8d4c78 100644 --- a/invokeai/frontend/web/src/features/nodes/components/ViewportControls.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/ViewportControls.tsx @@ -1,12 +1,12 @@ import { ButtonGroup } from '@chakra-ui/react'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { IAIIconButton } from 'exports'; -import { useCallback } from 'react'; +import { memo, useCallback } from 'react'; import { FaCode, FaExpand, FaMinus, FaPlus } from 'react-icons/fa'; import { useReactFlow } from 'reactflow'; import { shouldShowGraphOverlayChanged } from '../store/nodesSlice'; -export const ViewportControls = () => { +const ViewportControls = () => { const { zoomIn, zoomOut, fitView } = useReactFlow(); const dispatch = useAppDispatch(); const shouldShowGraphOverlay = useAppSelector( @@ -55,3 +55,5 @@ export const ViewportControls = () => { ); }; + +export default memo(ViewportControls); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputFieldComponent.tsx index 1dd4b1d8af..6f437dfcd8 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputFieldComponent.tsx @@ -2,13 +2,16 @@ import { ArrayInputFieldTemplate, ArrayInputFieldValue, } from 'features/nodes/types/types'; -import { FaImage, FaList } from 'react-icons/fa'; +import { memo } from 'react'; +import { FaList } from 'react-icons/fa'; import { FieldComponentProps } from './types'; -export const ArrayInputFieldComponent = ( +const ArrayInputFieldComponent = ( props: FieldComponentProps ) => { const { nodeId, field } = props; return ; }; + +export default memo(ArrayInputFieldComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/BooleanInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/BooleanInputFieldComponent.tsx index 948aa2bb0a..ceb2364e46 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/BooleanInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/BooleanInputFieldComponent.tsx @@ -5,10 +5,10 @@ import { BooleanInputFieldTemplate, BooleanInputFieldValue, } from 'features/nodes/types/types'; -import { ChangeEvent } from 'react'; +import { ChangeEvent, memo } from 'react'; import { FieldComponentProps } from './types'; -export const BooleanInputFieldComponent = ( +const BooleanInputFieldComponent = ( props: FieldComponentProps ) => { const { nodeId, field } = props; @@ -29,3 +29,5 @@ export const BooleanInputFieldComponent = ( ); }; + +export default memo(BooleanInputFieldComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/EnumInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/EnumInputFieldComponent.tsx index 87425a28f2..15602e7cad 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/EnumInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/EnumInputFieldComponent.tsx @@ -5,10 +5,10 @@ import { EnumInputFieldTemplate, EnumInputFieldValue, } from 'features/nodes/types/types'; -import { ChangeEvent } from 'react'; +import { ChangeEvent, memo } from 'react'; import { FieldComponentProps } from './types'; -export const EnumInputFieldComponent = ( +const EnumInputFieldComponent = ( props: FieldComponentProps ) => { const { nodeId, field, template } = props; @@ -33,3 +33,5 @@ export const EnumInputFieldComponent = ( ); }; + +export default memo(EnumInputFieldComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx index d07dfa4c6c..1dc0296139 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx @@ -9,12 +9,12 @@ import { ImageInputFieldTemplate, ImageInputFieldValue, } from 'features/nodes/types/types'; -import { DragEvent, useCallback, useState } from 'react'; +import { DragEvent, memo, useCallback, useState } from 'react'; import { FaImage } from 'react-icons/fa'; import { ImageType } from 'services/api'; import { FieldComponentProps } from './types'; -export const ImageInputFieldComponent = ( +const ImageInputFieldComponent = ( props: FieldComponentProps ) => { const { nodeId, field } = props; @@ -62,3 +62,5 @@ export const ImageInputFieldComponent = ( ); }; + +export default memo(ImageInputFieldComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/LatentsInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/LatentsInputFieldComponent.tsx index 1edf07e014..2de0a07eb5 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/LatentsInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/LatentsInputFieldComponent.tsx @@ -2,12 +2,15 @@ import { LatentsInputFieldTemplate, LatentsInputFieldValue, } from 'features/nodes/types/types'; +import { memo } from 'react'; import { FieldComponentProps } from './types'; -export const LatentsInputFieldComponent = ( +const LatentsInputFieldComponent = ( props: FieldComponentProps ) => { const { nodeId, field } = props; return null; }; + +export default memo(LatentsInputFieldComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx index b4f60acbd0..14f2816e1c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx @@ -12,7 +12,7 @@ import { selectModelsIds, } from 'features/system/store/modelSlice'; import { isEqual, map } from 'lodash'; -import { ChangeEvent } from 'react'; +import { ChangeEvent, memo } from 'react'; import { FieldComponentProps } from './types'; const availableModelsSelector = createSelector( @@ -28,7 +28,7 @@ const availableModelsSelector = createSelector( } ); -export const ModelInputFieldComponent = ( +const ModelInputFieldComponent = ( props: FieldComponentProps ) => { const { nodeId, field } = props; @@ -55,3 +55,5 @@ export const ModelInputFieldComponent = ( ); }; + +export default memo(ModelInputFieldComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/NumberInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/NumberInputFieldComponent.tsx index 3ef54a8a5a..f3c563f4fa 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/NumberInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/NumberInputFieldComponent.tsx @@ -13,9 +13,10 @@ import { IntegerInputFieldTemplate, IntegerInputFieldValue, } from 'features/nodes/types/types'; +import { memo } from 'react'; import { FieldComponentProps } from './types'; -export const NumberInputFieldComponent = ( +const NumberInputFieldComponent = ( props: FieldComponentProps< IntegerInputFieldValue | FloatInputFieldValue, IntegerInputFieldTemplate | FloatInputFieldTemplate @@ -39,3 +40,5 @@ export const NumberInputFieldComponent = ( ); }; + +export default memo(NumberInputFieldComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/StringInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/StringInputFieldComponent.tsx index 54341df31a..f371e8e58d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/StringInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/StringInputFieldComponent.tsx @@ -5,10 +5,10 @@ import { StringInputFieldTemplate, StringInputFieldValue, } from 'features/nodes/types/types'; -import { ChangeEvent } from 'react'; +import { ChangeEvent, memo } from 'react'; import { FieldComponentProps } from './types'; -export const StringInputFieldComponent = ( +const StringInputFieldComponent = ( props: FieldComponentProps ) => { const { nodeId, field } = props; @@ -27,3 +27,5 @@ export const StringInputFieldComponent = ( return ; }; + +export default memo(StringInputFieldComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/panels/BottomLeftPanel.tsx.tsx b/invokeai/frontend/web/src/features/nodes/components/panels/BottomLeftPanel.tsx.tsx index 7957acf271..fefad5f490 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panels/BottomLeftPanel.tsx.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/panels/BottomLeftPanel.tsx.tsx @@ -1,6 +1,6 @@ import { memo } from 'react'; import { Panel } from 'reactflow'; -import { ViewportControls } from '../ViewportControls'; +import ViewportControls from '../ViewportControls'; const BottomLeftPanel = () => ( diff --git a/invokeai/frontend/web/src/features/nodes/components/panels/TopLeftPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/panels/TopLeftPanel.tsx index 29e7cc1b14..2b89db000a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panels/TopLeftPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/panels/TopLeftPanel.tsx @@ -1,6 +1,6 @@ import { memo } from 'react'; import { Panel } from 'reactflow'; -import { AddNodeMenu } from '../AddNodeMenu'; +import AddNodeMenu from '../AddNodeMenu'; const TopLeftPanel = () => ( diff --git a/invokeai/frontend/web/src/features/nodes/components/panels/TopRightPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/panels/TopRightPanel.tsx index 094b7bb797..7e51e3e00e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panels/TopRightPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/panels/TopRightPanel.tsx @@ -2,7 +2,7 @@ import { RootState } from 'app/store'; import { useAppSelector } from 'app/storeHooks'; import { memo } from 'react'; import { Panel } from 'reactflow'; -import { FieldTypeLegend } from '../FieldTypeLegend'; +import FieldTypeLegend from '../FieldTypeLegend'; import NodeGraphOverlay from '../NodeGraphOverlay'; const TopRightPanel = () => { From 50e1ac731deb2e1e3eb5843a3f93b18eb5d266f5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 22 Apr 2023 22:23:18 +1000 Subject: [PATCH 7/7] fix(ui): make input/outputs renderfn callback --- .../nodes/components/IAINode/IAINodeInputs.tsx | 14 +++++--------- .../nodes/components/IAINode/IAINodeOutputs.tsx | 12 +++++------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeInputs.tsx b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeInputs.tsx index 61cdf67803..8c7aec680c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeInputs.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeInputs.tsx @@ -3,7 +3,7 @@ import { InputFieldValue, InvocationTemplate, } from 'features/nodes/types/types'; -import { memo, MutableRefObject, ReactNode } from 'react'; +import { memo, ReactNode, useCallback } from 'react'; import { map } from 'lodash'; import { useAppSelector } from 'app/storeHooks'; import { RootState } from 'app/store'; @@ -14,13 +14,11 @@ import { FormLabel, HStack, Tooltip, - Icon, Divider, } from '@chakra-ui/react'; import FieldHandle from '../FieldHandle'; import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection'; import InputFieldComponent from '../InputFieldComponent'; -import { FaInfoCircle } from 'react-icons/fa'; import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; interface IAINodeInputProps { @@ -102,11 +100,9 @@ interface IAINodeInputsProps { const IAINodeInputs = (props: IAINodeInputsProps) => { const { nodeId, template, inputs } = props; - const connectedInputs = useAppSelector( - (state: RootState) => state.nodes.edges - ); + const edges = useAppSelector((state: RootState) => state.nodes.edges); - const renderIAINodeInputs = () => { + const renderIAINodeInputs = useCallback(() => { const IAINodeInputsToRender: ReactNode[] = []; const inputSockets = map(inputs); @@ -114,7 +110,7 @@ const IAINodeInputs = (props: IAINodeInputsProps) => { const inputTemplate = template.inputs[inputSocket.name]; const isConnected = Boolean( - connectedInputs.filter((connectedInput) => { + edges.filter((connectedInput) => { return ( connectedInput.target === nodeId && connectedInput.targetHandle === inputSocket.name @@ -142,7 +138,7 @@ const IAINodeInputs = (props: IAINodeInputsProps) => { {IAINodeInputsToRender} ); - }; + }, [edges, inputs, nodeId, template.inputs]); return renderIAINodeInputs(); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeOutputs.tsx b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeOutputs.tsx index a17eaa0946..38a3a169b8 100644 --- a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeOutputs.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeOutputs.tsx @@ -3,7 +3,7 @@ import { OutputFieldTemplate, OutputFieldValue, } from 'features/nodes/types/types'; -import { memo, MutableRefObject, ReactNode } from 'react'; +import { memo, ReactNode, useCallback } from 'react'; import { map } from 'lodash'; import { useAppSelector } from 'app/storeHooks'; import { RootState } from 'app/store'; @@ -59,11 +59,9 @@ interface IAINodeOutputsProps { const IAINodeOutputs = (props: IAINodeOutputsProps) => { const { nodeId, template, outputs } = props; - const connectedInputs = useAppSelector( - (state: RootState) => state.nodes.edges - ); + const edges = useAppSelector((state: RootState) => state.nodes.edges); - const renderIAINodeOutputs = () => { + const renderIAINodeOutputs = useCallback(() => { const IAINodeOutputsToRender: ReactNode[] = []; const outputSockets = map(outputs); @@ -71,7 +69,7 @@ const IAINodeOutputs = (props: IAINodeOutputsProps) => { const outputTemplate = template.outputs[outputSocket.name]; const isConnected = Boolean( - connectedInputs.filter((connectedInput) => { + edges.filter((connectedInput) => { return ( connectedInput.source === nodeId && connectedInput.sourceHandle === outputSocket.name @@ -91,7 +89,7 @@ const IAINodeOutputs = (props: IAINodeOutputsProps) => { }); return {IAINodeOutputsToRender}; - }; + }, [edges, nodeId, outputs, template.outputs]); return renderIAINodeOutputs(); };