Merge branch 'main' into mm-ui

This commit is contained in:
blessedcoolant 2023-07-13 13:58:22 +12:00
commit 1c2144794c
12 changed files with 323 additions and 39 deletions

View File

@ -532,7 +532,7 @@
"hidePreview": "Hide Preview",
"showPreview": "Show Preview",
"controlNetControlMode": "Control Mode",
"clipSkip": "Clip Skip",
"clipSkip": "CLIP Skip",
"aspectRatio": "Ratio"
},
"settings": {
@ -597,7 +597,10 @@
"metadataLoadFailed": "Failed to load metadata",
"initialImageSet": "Initial Image Set",
"initialImageNotSet": "Initial Image Not Set",
"initialImageNotSetDesc": "Could not load initial image"
"initialImageNotSetDesc": "Could not load initial image",
"nodesSaved": "Nodes Saved",
"nodesLoaded": "Nodes Loaded",
"nodesLoadedFailed": "Failed To Load Nodes"
},
"tooltip": {
"feature": {
@ -680,6 +683,8 @@
"swapSizes": "Swap Sizes"
},
"nodes": {
"reloadSchema": "Reload Schema"
"reloadSchema": "Reload Schema",
"saveNodes": "Save Nodes",
"loadNodes": "Load Nodes"
}
}

View File

@ -12,6 +12,7 @@ import {
setIsMovingBoundingBox,
setIsTransformingBoundingBox,
} from 'features/canvas/store/canvasSlice';
import { uiSelector } from 'features/ui/store/uiSelectors';
import Konva from 'konva';
import { GroupConfig } from 'konva/lib/Group';
import { KonvaEventObject } from 'konva/lib/Node';
@ -22,8 +23,8 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { Group, Rect, Transformer } from 'react-konva';
const boundingBoxPreviewSelector = createSelector(
canvasSelector,
(canvas) => {
[canvasSelector, uiSelector],
(canvas, ui) => {
const {
boundingBoxCoordinates,
boundingBoxDimensions,
@ -35,6 +36,8 @@ const boundingBoxPreviewSelector = createSelector(
shouldSnapToGrid,
} = canvas;
const { aspectRatio } = ui;
return {
boundingBoxCoordinates,
boundingBoxDimensions,
@ -45,6 +48,7 @@ const boundingBoxPreviewSelector = createSelector(
shouldSnapToGrid,
tool,
hitStrokeWidth: 20 / stageScale,
aspectRatio,
};
},
{
@ -70,6 +74,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
shouldSnapToGrid,
tool,
hitStrokeWidth,
aspectRatio,
} = useAppSelector(boundingBoxPreviewSelector);
const transformerRef = useRef<Konva.Transformer>(null);
@ -137,12 +142,22 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
const x = Math.round(rect.x());
const y = Math.round(rect.y());
dispatch(
setBoundingBoxDimensions({
width,
height,
})
);
if (aspectRatio) {
const newHeight = roundToMultiple(width / aspectRatio, 64);
dispatch(
setBoundingBoxDimensions({
width: width,
height: newHeight,
})
);
} else {
dispatch(
setBoundingBoxDimensions({
width,
height,
})
);
}
dispatch(
setBoundingBoxCoordinates({
@ -154,7 +169,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
// Reset the scale now that the coords/dimensions have been un-scaled
rect.scaleX(1);
rect.scaleY(1);
}, [dispatch, shouldSnapToGrid]);
}, [dispatch, shouldSnapToGrid, aspectRatio]);
const anchorDragBoundFunc = useCallback(
(

View File

@ -7,7 +7,14 @@ import {
import { IRect, Vector2d } from 'konva/lib/types';
import { clamp, cloneDeep } from 'lodash-es';
//
import {
setActiveTab,
setAspectRatio,
setShouldUseCanvasBetaLayout,
} from 'features/ui/store/uiSlice';
import { RgbaColor } from 'react-colorful';
import { sessionCanceled } from 'services/api/thunks/session';
import { ImageDTO } from 'services/api/types';
import calculateCoordinates from '../util/calculateCoordinates';
import calculateScale from '../util/calculateScale';
import { STAGE_PADDING_PERCENTAGE } from '../util/constants';
@ -28,13 +35,6 @@ import {
isCanvasBaseImage,
isCanvasMaskLine,
} from './canvasTypes';
import { ImageDTO } from 'services/api/types';
import { sessionCanceled } from 'services/api/thunks/session';
import {
setActiveTab,
setShouldUseCanvasBetaLayout,
} from 'features/ui/store/uiSlice';
import { imageUrlsReceived } from 'services/api/thunks/image';
export const initialLayerState: CanvasLayerState = {
objects: [],
@ -240,6 +240,16 @@ export const canvasSlice = createSlice({
state.scaledBoundingBoxDimensions = scaledDimensions;
}
},
flipBoundingBoxAxes: (state) => {
const [currWidth, currHeight] = [
state.boundingBoxDimensions.width,
state.boundingBoxDimensions.height,
];
state.boundingBoxDimensions = {
width: currHeight,
height: currWidth,
};
},
setBoundingBoxCoordinates: (state, action: PayloadAction<Vector2d>) => {
state.boundingBoxCoordinates = floorCoordinates(action.payload);
},
@ -864,6 +874,15 @@ export const canvasSlice = createSlice({
builder.addCase(setActiveTab, (state, action) => {
state.doesCanvasNeedScaling = true;
});
builder.addCase(setAspectRatio, (state, action) => {
const ratio = action.payload;
if (ratio) {
state.boundingBoxDimensions.height = roundToMultiple(
state.boundingBoxDimensions.width / ratio,
64
);
}
});
// builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
// const { image_name, image_url, thumbnail_url } = action.payload;
@ -912,6 +931,7 @@ export const {
setBoundingBoxDimensions,
setBoundingBoxPreviewFill,
setBoundingBoxScaleMethod,
flipBoundingBoxAxes,
setBrushColor,
setBrushSize,
setCanvasContainerDimensions,

View File

@ -10,7 +10,6 @@ import {
OnInit,
OnNodesChange,
ReactFlow,
ReactFlowInstance,
} from 'reactflow';
import {
connectionEnded,
@ -18,6 +17,7 @@ import {
connectionStarted,
edgesChanged,
nodesChanged,
setEditorInstance,
} from '../store/nodesSlice';
import { InvocationComponent } from './InvocationComponent';
import ProgressImageNode from './ProgressImageNode';
@ -69,11 +69,13 @@ export const Flow = () => {
dispatch(connectionEnded());
}, [dispatch]);
const onInit: OnInit = useCallback((v: ReactFlowInstance) => {
if (v) {
v.fitView();
}
}, []);
const onInit: OnInit = useCallback(
(v) => {
dispatch(setEditorInstance(v));
if (v) v.fitView();
},
[dispatch]
);
return (
<ReactFlow

View File

@ -1,10 +1,11 @@
import { HStack } from '@chakra-ui/react';
import CancelButton from 'features/parameters/components/ProcessButtons/CancelButton';
import { memo } from 'react';
import { Panel } from 'reactflow';
import CancelButton from 'features/parameters/components/ProcessButtons/CancelButton';
import LoadNodesButton from '../ui/LoadNodesButton';
import NodeInvokeButton from '../ui/NodeInvokeButton';
import ReloadSchemaButton from '../ui/ReloadSchemaButton';
import SaveNodesButton from '../ui/SaveNodesButton';
const TopCenterPanel = () => {
return (
@ -13,6 +14,8 @@ const TopCenterPanel = () => {
<NodeInvokeButton />
<CancelButton />
<ReloadSchemaButton />
<SaveNodesButton />
<LoadNodesButton />
</HStack>
</Panel>
);

View File

@ -0,0 +1,79 @@
import { FileButton } from '@mantine/core';
import { makeToast } from 'app/components/Toaster';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { loadFileEdges, loadFileNodes } from 'features/nodes/store/nodesSlice';
import { addToast } from 'features/system/store/systemSlice';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { FaUpload } from 'react-icons/fa';
import { useReactFlow } from 'reactflow';
const LoadNodesButton = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { fitView } = useReactFlow();
const uploadedFileRef = useRef<() => void>(null);
const restoreJSONToEditor = useCallback(
(v: File | null) => {
if (!v) return;
const reader = new FileReader();
reader.onload = async () => {
const json = reader.result;
const retrievedNodeTree = await JSON.parse(String(json));
if (!retrievedNodeTree) {
dispatch(
addToast(
makeToast({
title: t('toast.nodesLoadedFailed'),
status: 'error',
})
)
);
}
if (retrievedNodeTree) {
dispatch(loadFileNodes(retrievedNodeTree.nodes));
dispatch(loadFileEdges(retrievedNodeTree.edges));
fitView();
dispatch(
addToast(
makeToast({ title: t('toast.nodesLoaded'), status: 'success' })
)
);
}
// Cleanup
reader.abort();
};
reader.readAsText(v);
// Cleanup
uploadedFileRef.current?.();
},
[fitView, dispatch, t]
);
return (
<FileButton
resetRef={uploadedFileRef}
accept="application/json"
onChange={restoreJSONToEditor}
>
{(props) => (
<IAIIconButton
icon={<FaUpload />}
tooltip={t('nodes.loadNodes')}
aria-label={t('nodes.loadNodes')}
{...props}
/>
)}
</FileButton>
);
};
export default memo(LoadNodesButton);

View File

@ -0,0 +1,45 @@
import { RootState } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { map, omit } from 'lodash-es';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { FaSave } from 'react-icons/fa';
const SaveNodesButton = () => {
const { t } = useTranslation();
const editorInstance = useAppSelector(
(state: RootState) => state.nodes.editorInstance
);
const saveEditorToJSON = useCallback(() => {
if (editorInstance) {
const editorState = editorInstance.toObject();
editorState.edges = map(editorState.edges, (edge) => {
return omit(edge, ['style']);
});
const nodeSetupJSON = new Blob([JSON.stringify(editorState)]);
const nodeDownloadElement = document.createElement('a');
nodeDownloadElement.href = URL.createObjectURL(nodeSetupJSON);
nodeDownloadElement.download = 'MyNodes.json';
document.body.appendChild(nodeDownloadElement);
nodeDownloadElement.click();
// Cleanup
nodeDownloadElement.remove();
}
}, [editorInstance]);
return (
<IAIIconButton
icon={<FaSave />}
fontSize={18}
tooltip={t('nodes.saveNodes')}
aria-label={t('nodes.saveNodes')}
onClick={saveEditorToJSON}
/>
);
};
export default memo(SaveNodesButton);

View File

@ -13,6 +13,7 @@ import {
Node,
NodeChange,
OnConnectStartParams,
ReactFlowInstance,
} from 'reactflow';
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
import { ImageField } from 'services/api/types';
@ -25,6 +26,7 @@ export type NodesState = {
invocationTemplates: Record<string, InvocationTemplate>;
connectionStartParams: OnConnectStartParams | null;
shouldShowGraphOverlay: boolean;
editorInstance: ReactFlowInstance | undefined;
};
export const initialNodesState: NodesState = {
@ -34,6 +36,7 @@ export const initialNodesState: NodesState = {
invocationTemplates: {},
connectionStartParams: null,
shouldShowGraphOverlay: false,
editorInstance: undefined,
};
const nodesSlice = createSlice({
@ -121,6 +124,15 @@ const nodesSlice = createSlice({
nodeEditorReset: () => {
return { ...initialNodesState };
},
setEditorInstance: (state, action) => {
state.editorInstance = action.payload;
},
loadFileNodes: (state, action: PayloadAction<Node<InvocationValue>[]>) => {
state.nodes = action.payload;
},
loadFileEdges: (state, action: PayloadAction<Edge[]>) => {
state.edges = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => {
@ -141,6 +153,9 @@ export const {
nodeTemplatesBuilt,
nodeEditorReset,
imageCollectionFieldValueChanged,
setEditorInstance,
loadFileNodes,
loadFileEdges,
} = nodesSlice.actions;
export default nodesSlice.reducer;

View File

@ -2,22 +2,26 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAISlider from 'common/components/IAISlider';
import { roundToMultiple } from 'common/util/roundDownToMultiple';
import {
canvasSelector,
isStagingSelector,
} from 'features/canvas/store/canvasSelectors';
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const selector = createSelector(
[canvasSelector, isStagingSelector],
(canvas, isStaging) => {
[canvasSelector, isStagingSelector, uiSelector],
(canvas, isStaging, ui) => {
const { boundingBoxDimensions } = canvas;
const { aspectRatio } = ui;
return {
boundingBoxDimensions,
isStaging,
aspectRatio,
};
},
defaultSelectorOptions
@ -25,7 +29,8 @@ const selector = createSelector(
const ParamBoundingBoxWidth = () => {
const dispatch = useAppDispatch();
const { boundingBoxDimensions, isStaging } = useAppSelector(selector);
const { boundingBoxDimensions, isStaging, aspectRatio } =
useAppSelector(selector);
const { t } = useTranslation();
@ -36,6 +41,15 @@ const ParamBoundingBoxWidth = () => {
height: Math.floor(v),
})
);
if (aspectRatio) {
const newWidth = roundToMultiple(v * aspectRatio, 64);
dispatch(
setBoundingBoxDimensions({
width: newWidth,
height: Math.floor(v),
})
);
}
};
const handleResetHeight = () => {
@ -45,6 +59,15 @@ const ParamBoundingBoxWidth = () => {
height: Math.floor(512),
})
);
if (aspectRatio) {
const newWidth = roundToMultiple(512 * aspectRatio, 64);
dispatch(
setBoundingBoxDimensions({
width: newWidth,
height: Math.floor(512),
})
);
}
};
return (

View File

@ -0,0 +1,57 @@
import { Flex, Spacer, Text } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { flipBoundingBoxAxes } from 'features/canvas/store/canvasSlice';
import { useTranslation } from 'react-i18next';
import { MdOutlineSwapVert } from 'react-icons/md';
import ParamAspectRatio from '../../Core/ParamAspectRatio';
import ParamBoundingBoxHeight from './ParamBoundingBoxHeight';
import ParamBoundingBoxWidth from './ParamBoundingBoxWidth';
export default function ParamBoundingBoxSize() {
const dispatch = useAppDispatch();
const { t } = useTranslation();
return (
<Flex
sx={{
gap: 2,
p: 4,
borderRadius: 4,
flexDirection: 'column',
w: 'full',
bg: 'base.150',
_dark: {
bg: 'base.750',
},
}}
>
<Flex alignItems="center" gap={2}>
<Text
sx={{
fontSize: 'sm',
width: 'full',
color: 'base.700',
_dark: {
color: 'base.300',
},
}}
>
{t('parameters.aspectRatio')}
</Text>
<Spacer />
<ParamAspectRatio />
<IAIIconButton
tooltip={t('ui.swapSizes')}
aria-label={t('ui.swapSizes')}
size="sm"
icon={<MdOutlineSwapVert />}
fontSize={20}
onClick={() => dispatch(flipBoundingBoxAxes())}
/>
</Flex>
<ParamBoundingBoxWidth />
<ParamBoundingBoxHeight />
</Flex>
);
}

View File

@ -2,22 +2,26 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAISlider from 'common/components/IAISlider';
import { roundToMultiple } from 'common/util/roundDownToMultiple';
import {
canvasSelector,
isStagingSelector,
} from 'features/canvas/store/canvasSelectors';
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const selector = createSelector(
[canvasSelector, isStagingSelector],
(canvas, isStaging) => {
[canvasSelector, isStagingSelector, uiSelector],
(canvas, isStaging, ui) => {
const { boundingBoxDimensions } = canvas;
const { aspectRatio } = ui;
return {
boundingBoxDimensions,
isStaging,
aspectRatio,
};
},
defaultSelectorOptions
@ -25,7 +29,8 @@ const selector = createSelector(
const ParamBoundingBoxWidth = () => {
const dispatch = useAppDispatch();
const { boundingBoxDimensions, isStaging } = useAppSelector(selector);
const { boundingBoxDimensions, isStaging, aspectRatio } =
useAppSelector(selector);
const { t } = useTranslation();
@ -36,6 +41,15 @@ const ParamBoundingBoxWidth = () => {
width: Math.floor(v),
})
);
if (aspectRatio) {
const newHeight = roundToMultiple(v / aspectRatio, 64);
dispatch(
setBoundingBoxDimensions({
width: Math.floor(v),
height: newHeight,
})
);
}
};
const handleResetWidth = () => {
@ -45,6 +59,15 @@ const ParamBoundingBoxWidth = () => {
width: Math.floor(512),
})
);
if (aspectRatio) {
const newHeight = roundToMultiple(512 / aspectRatio, 64);
dispatch(
setBoundingBoxDimensions({
width: Math.floor(512),
height: newHeight,
})
);
}
};
return (

View File

@ -4,8 +4,7 @@ import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAICollapse from 'common/components/IAICollapse';
import ParamBoundingBoxHeight from 'features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxHeight';
import ParamBoundingBoxWidth from 'features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxWidth';
import ParamBoundingBoxSize from 'features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxSize';
import ParamCFGScale from 'features/parameters/components/Parameters/Core/ParamCFGScale';
import ParamIterations from 'features/parameters/components/Parameters/Core/ParamIterations';
import ParamModelandVAEandScheduler from 'features/parameters/components/Parameters/Core/ParamModelandVAEandScheduler';
@ -51,8 +50,7 @@ const UnifiedCanvasCoreParameters = () => {
<Box pt={2}>
<ParamSeedFull />
</Box>
<ParamBoundingBoxWidth />
<ParamBoundingBoxHeight />
<ParamBoundingBoxSize />
</>
) : (
<>
@ -65,8 +63,7 @@ const UnifiedCanvasCoreParameters = () => {
<Box pt={2}>
<ParamSeedFull />
</Box>
<ParamBoundingBoxWidth />
<ParamBoundingBoxHeight />
<ParamBoundingBoxSize />
</>
)}
<ImageToImageStrength />