Initial unification of canvas

This commit is contained in:
psychedelicious 2022-11-17 09:56:48 +11:00 committed by blessedcoolant
parent caf8f0ae35
commit 15dd1339d2
19 changed files with 580 additions and 296 deletions

View File

@ -8,6 +8,7 @@ import math
import io import io
import base64 import base64
import os import os
import json
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from flask import Flask, redirect, send_from_directory, request, make_response from flask import Flask, redirect, send_from_directory, request, make_response
@ -107,6 +108,7 @@ class InvokeAIWebServer:
@self.app.route("/upload", methods=["POST"]) @self.app.route("/upload", methods=["POST"])
def upload(): def upload():
try: try:
data = json.loads(request.form["data"])
filename = "" filename = ""
# check if the post request has the file part # check if the post request has the file part
if "file" in request.files: if "file" in request.files:
@ -116,15 +118,15 @@ class InvokeAIWebServer:
if file.filename == "": if file.filename == "":
return make_response("No file selected", 400) return make_response("No file selected", 400)
filename = file.filename filename = file.filename
elif "dataURL" in request.form: elif "dataURL" in data:
file = dataURL_to_bytes(request.form["dataURL"]) file = dataURL_to_bytes(data["dataURL"])
if "filename" not in request.form or request.form["filename"] == "": if "filename" not in data or data["filename"] == "":
return make_response("No filename provided", 400) return make_response("No filename provided", 400)
filename = request.form["filename"] filename = data["filename"]
else: else:
return make_response("No file or dataURL", 400) return make_response("No file or dataURL", 400)
kind = request.form["kind"] kind = data["kind"]
if kind == "init": if kind == "init":
path = self.init_image_path path = self.init_image_path
@ -153,22 +155,32 @@ class InvokeAIWebServer:
file_path = os.path.join(path, name) file_path = os.path.join(path, name)
if "dataURL" in request.form: if "dataURL" in data:
with open(file_path, "wb") as f: with open(file_path, "wb") as f:
f.write(file) f.write(file)
else: else:
file.save(file_path) file.save(file_path)
mtime = os.path.getmtime(file_path) mtime = os.path.getmtime(file_path)
(width, height) = Image.open(file_path).size
pil_image = Image.open(file_path)
# visible_image_bbox = pil_image.getbbox()
# pil_image = pil_image.crop(visible_image_bbox)
# pil_image.save(file_path)
# if "cropVisible" in data and data["cropVisible"] == True:
# visible_image_bbox = pil_image.getbbox()
# pil_image = pil_image.crop(visible_image_bbox)
# pil_image.save(file_path)
(width, height) = pil_image.size
response = { response = {
"image": {
"url": self.get_url_from_image_path(file_path), "url": self.get_url_from_image_path(file_path),
"mtime": mtime, "mtime": mtime,
# "bbox": visible_image_bbox,
"width": width, "width": width,
"height": height, "height": height,
},
} }
return make_response(response, 200) return make_response(response, 200)

View File

@ -179,7 +179,12 @@ export declare type ImageResultResponse = Omit<Image, 'uuid'> & {
}; };
export declare type ImageUploadResponse = { export declare type ImageUploadResponse = {
image: Omit<Image, 'uuid' | 'metadata' | 'category'>; // image: Omit<Image, 'uuid' | 'metadata' | 'category'>;
url: string;
mtime: number;
width: number;
height: number;
// bbox: [number, number, number, number];
}; };
export declare type ErrorResponse = { export declare type ErrorResponse = {

View File

@ -82,8 +82,6 @@ const rootPersistConfig = getPersistConfig({
debounce: 300, debounce: 300,
}); });
// console.log(rootPersistConfig)
const persistedReducer = persistReducer(rootPersistConfig, rootReducer); const persistedReducer = persistReducer(rootPersistConfig, rootReducer);
// Continue with store setup // Continue with store setup
@ -95,6 +93,7 @@ export const store = configureStore({
serializableCheck: false, serializableCheck: false,
}).concat(socketioMiddleware()), }).concat(socketioMiddleware()),
devTools: { devTools: {
// Uncommenting these very rapidly called actions makes the redux dev tools output much more readable
actionsDenylist: [ actionsDenylist: [
// 'canvas/setCursorPosition', // 'canvas/setCursorPosition',
// 'canvas/setStageCoordinates', // 'canvas/setStageCoordinates',

View File

@ -1,16 +1,19 @@
// lib // lib
import { MutableRefObject, useRef } from 'react'; import { MutableRefObject, useCallback, useRef } from 'react';
import Konva from 'konva'; import Konva from 'konva';
import { Layer, Stage } from 'react-konva'; import { Layer, Stage } from 'react-konva';
import { Stage as StageType } from 'konva/lib/Stage'; import { Stage as StageType } from 'konva/lib/Stage';
// app // app
import { useAppSelector } from 'app/store'; import { RootState, useAppDispatch, useAppSelector } from 'app/store';
import { import {
baseCanvasImageSelector, baseCanvasImageSelector,
currentCanvasSelector, currentCanvasSelector,
isStagingSelector, isStagingSelector,
outpaintingCanvasSelector, outpaintingCanvasSelector,
setStageCoordinates,
setStageScale,
shouldLockToInitialImageSelector,
} from 'features/canvas/canvasSlice'; } from 'features/canvas/canvasSlice';
// component // component
@ -35,15 +38,31 @@ import IAICanvasGrid from './IAICanvasGrid';
import IAICanvasIntermediateImage from './IAICanvasIntermediateImage'; import IAICanvasIntermediateImage from './IAICanvasIntermediateImage';
import IAICanvasStatusText from './IAICanvasStatusText'; import IAICanvasStatusText from './IAICanvasStatusText';
import IAICanvasStagingArea from './IAICanvasStagingArea'; import IAICanvasStagingArea from './IAICanvasStagingArea';
import IAICanvasStagingAreaToolbar from './IAICanvasStagingAreaToolbar';
import { KonvaEventObject } from 'konva/lib/Node';
import {
CANVAS_SCALE_BY,
MAX_CANVAS_SCALE,
MIN_CANVAS_SCALE,
} from './util/constants';
const canvasSelector = createSelector( const canvasSelector = createSelector(
[ [
shouldLockToInitialImageSelector,
currentCanvasSelector, currentCanvasSelector,
outpaintingCanvasSelector, outpaintingCanvasSelector,
isStagingSelector, isStagingSelector,
activeTabNameSelector, activeTabNameSelector,
baseCanvasImageSelector,
], ],
(currentCanvas, outpaintingCanvas, isStaging, activeTabName) => { (
shouldLockToInitialImage,
currentCanvas,
outpaintingCanvas,
isStaging,
activeTabName,
baseCanvasImage
) => {
const { const {
isMaskEnabled, isMaskEnabled,
stageScale, stageScale,
@ -56,6 +75,7 @@ const canvasSelector = createSelector(
tool, tool,
isMovingStage, isMovingStage,
shouldShowIntermediates, shouldShowIntermediates,
minimumStageScale,
} = currentCanvas; } = currentCanvas;
const { shouldShowGrid } = outpaintingCanvas; const { shouldShowGrid } = outpaintingCanvas;
@ -89,6 +109,10 @@ const canvasSelector = createSelector(
isOnOutpaintingTab: activeTabName === 'outpainting', isOnOutpaintingTab: activeTabName === 'outpainting',
isStaging, isStaging,
shouldShowIntermediates, shouldShowIntermediates,
shouldLockToInitialImage,
activeTabName,
minimumStageScale,
baseCanvasImage,
}; };
}, },
{ {
@ -116,8 +140,12 @@ const IAICanvas = () => {
isOnOutpaintingTab, isOnOutpaintingTab,
isStaging, isStaging,
shouldShowIntermediates, shouldShowIntermediates,
shouldLockToInitialImage,
activeTabName,
minimumStageScale,
baseCanvasImage,
} = useAppSelector(canvasSelector); } = useAppSelector(canvasSelector);
const dispatch = useAppDispatch();
useCanvasHotkeys(); useCanvasHotkeys();
// set the closure'd refs // set the closure'd refs
@ -142,6 +170,34 @@ const IAICanvas = () => {
const { handleDragStart, handleDragMove, handleDragEnd } = const { handleDragStart, handleDragMove, handleDragEnd } =
useCanvasDragMove(); useCanvasDragMove();
const dragBoundFunc = useCallback(
(newCoordinates: Vector2d) => {
if (shouldLockToInitialImage && baseCanvasImage) {
newCoordinates.x = _.clamp(
newCoordinates.x,
stageDimensions.width -
Math.floor(baseCanvasImage.width * stageScale),
0
);
newCoordinates.y = _.clamp(
newCoordinates.y,
stageDimensions.height -
Math.floor(baseCanvasImage.height * stageScale),
0
);
}
return newCoordinates;
},
[
baseCanvasImage,
shouldLockToInitialImage,
stageDimensions.height,
stageDimensions.width,
stageScale,
]
);
return ( return (
<div className="inpainting-canvas-container"> <div className="inpainting-canvas-container">
<div className="inpainting-canvas-wrapper"> <div className="inpainting-canvas-wrapper">
@ -157,6 +213,7 @@ const IAICanvas = () => {
width={stageDimensions.width} width={stageDimensions.width}
height={stageDimensions.height} height={stageDimensions.height}
scale={{ x: stageScale, y: stageScale }} scale={{ x: stageScale, y: stageScale }}
dragBoundFunc={dragBoundFunc}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseOut} onMouseLeave={handleMouseOut}
@ -205,6 +262,7 @@ const IAICanvas = () => {
</Layer> </Layer>
</Stage> </Stage>
{isOnOutpaintingTab && <IAICanvasStatusText />} {isOnOutpaintingTab && <IAICanvasStatusText />}
<IAICanvasStagingAreaToolbar />
</div> </div>
</div> </div>
); );

View File

@ -6,8 +6,11 @@ import { Vector2d } from 'konva/lib/types';
import _ from 'lodash'; import _ from 'lodash';
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { Group, Rect, Transformer } from 'react-konva'; import { Group, Rect, Transformer } from 'react-konva';
import { useAppDispatch, useAppSelector } from 'app/store'; import { RootState, useAppDispatch, useAppSelector } from 'app/store';
import { roundToMultiple } from 'common/util/roundDownToMultiple'; import {
roundDownToMultiple,
roundToMultiple,
} from 'common/util/roundDownToMultiple';
import { import {
baseCanvasImageSelector, baseCanvasImageSelector,
currentCanvasSelector, currentCanvasSelector,
@ -17,16 +20,24 @@ import {
setIsMouseOverBoundingBox, setIsMouseOverBoundingBox,
setIsMovingBoundingBox, setIsMovingBoundingBox,
setIsTransformingBoundingBox, setIsTransformingBoundingBox,
shouldLockToInitialImageSelector,
} from 'features/canvas/canvasSlice'; } from 'features/canvas/canvasSlice';
import { GroupConfig } from 'konva/lib/Group'; import { GroupConfig } from 'konva/lib/Group';
import { activeTabNameSelector } from 'features/options/optionsSelectors'; import { activeTabNameSelector } from 'features/options/optionsSelectors';
const boundingBoxPreviewSelector = createSelector( const boundingBoxPreviewSelector = createSelector(
shouldLockToInitialImageSelector,
currentCanvasSelector, currentCanvasSelector,
outpaintingCanvasSelector, outpaintingCanvasSelector,
baseCanvasImageSelector, baseCanvasImageSelector,
activeTabNameSelector, activeTabNameSelector,
(currentCanvas, outpaintingCanvas, baseCanvasImage, activeTabName) => { (
shouldLockToInitialImage,
currentCanvas,
outpaintingCanvas,
baseCanvasImage,
activeTabName
) => {
const { const {
boundingBoxCoordinates, boundingBoxCoordinates,
boundingBoxDimensions, boundingBoxDimensions,
@ -40,6 +51,7 @@ const boundingBoxPreviewSelector = createSelector(
tool, tool,
stageCoordinates, stageCoordinates,
} = currentCanvas; } = currentCanvas;
const { shouldSnapToGrid } = outpaintingCanvas; const { shouldSnapToGrid } = outpaintingCanvas;
return { return {
@ -59,6 +71,7 @@ const boundingBoxPreviewSelector = createSelector(
stageCoordinates, stageCoordinates,
boundingBoxStrokeWidth: (isMouseOverBoundingBox ? 8 : 1) / stageScale, boundingBoxStrokeWidth: (isMouseOverBoundingBox ? 8 : 1) / stageScale,
hitStrokeWidth: 20 / stageScale, hitStrokeWidth: 20 / stageScale,
shouldLockToInitialImage,
}; };
}, },
{ {
@ -91,6 +104,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
tool, tool,
boundingBoxStrokeWidth, boundingBoxStrokeWidth,
hitStrokeWidth, hitStrokeWidth,
shouldLockToInitialImage,
} = useAppSelector(boundingBoxPreviewSelector); } = useAppSelector(boundingBoxPreviewSelector);
const transformerRef = useRef<Konva.Transformer>(null); const transformerRef = useRef<Konva.Transformer>(null);
@ -106,7 +120,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
const handleOnDragMove = useCallback( const handleOnDragMove = useCallback(
(e: KonvaEventObject<DragEvent>) => { (e: KonvaEventObject<DragEvent>) => {
if (activeTabName === 'inpainting' || !shouldSnapToGrid) { if (!shouldSnapToGrid) {
dispatch( dispatch(
setBoundingBoxCoordinates({ setBoundingBoxCoordinates({
x: Math.floor(e.target.x()), x: Math.floor(e.target.x()),
@ -132,20 +146,27 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
}) })
); );
}, },
[activeTabName, dispatch, shouldSnapToGrid] [dispatch, shouldSnapToGrid]
); );
const dragBoundFunc = useCallback( const dragBoundFunc = useCallback(
(position: Vector2d) => { (position: Vector2d) => {
if (!baseCanvasImage && activeTabName !== 'outpainting') /**
return boundingBoxCoordinates; * This limits the bounding box's drag coordinates.
*/
if (!shouldLockToInitialImage) return boundingBoxCoordinates;
const { x, y } = position; const { x, y } = position;
const maxX = const maxX =
stageDimensions.width - boundingBoxDimensions.width * stageScale; stageDimensions.width -
boundingBoxDimensions.width -
(stageDimensions.width % 64);
const maxY = const maxY =
stageDimensions.height - boundingBoxDimensions.height * stageScale; stageDimensions.height -
boundingBoxDimensions.height -
(stageDimensions.height % 64);
const clampedX = Math.floor(_.clamp(x, 0, maxX)); const clampedX = Math.floor(_.clamp(x, 0, maxX));
const clampedY = Math.floor(_.clamp(y, 0, maxY)); const clampedY = Math.floor(_.clamp(y, 0, maxY));
@ -153,14 +174,12 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
return { x: clampedX, y: clampedY }; return { x: clampedX, y: clampedY };
}, },
[ [
baseCanvasImage, shouldLockToInitialImage,
activeTabName,
boundingBoxCoordinates, boundingBoxCoordinates,
stageDimensions.width, stageDimensions.width,
stageDimensions.height, stageDimensions.height,
boundingBoxDimensions.width, boundingBoxDimensions.width,
boundingBoxDimensions.height, boundingBoxDimensions.height,
stageScale,
] ]
); );
@ -203,7 +222,6 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
rect.scaleY(1); rect.scaleY(1);
}, [dispatch]); }, [dispatch]);
// OK
const anchorDragBoundFunc = useCallback( const anchorDragBoundFunc = useCallback(
( (
oldPos: Vector2d, // old absolute position of anchor point oldPos: Vector2d, // old absolute position of anchor point
@ -215,49 +233,14 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
* Konva does not transform with width or height. It transforms the anchor point * Konva does not transform with width or height. It transforms the anchor point
* and scale factor. This is then sent to the shape's onTransform listeners. * and scale factor. This is then sent to the shape's onTransform listeners.
* *
* We need to snap the new width to steps of 64 without also snapping the * We need to snap the new dimensions to steps of 64. But because the whole
* coordinates of the bounding box to steps of 64. But because the whole
* stage is scaled, our actual desired step is actually 64 * the stage scale. * stage is scaled, our actual desired step is actually 64 * the stage scale.
*/ */
// Difference of the old coords from the nearest multiple the scaled step return {
const offsetX = oldPos.x % scaledStep; x: roundToMultiple(newPos.x, scaledStep),
const offsetY = oldPos.y % scaledStep; y: roundToMultiple(newPos.y, scaledStep),
// Round new position to the nearest multiple of the scaled step
const closestX = roundToMultiple(newPos.x, scaledStep) + offsetX;
const closestY = roundToMultiple(newPos.y, scaledStep) + offsetY;
// the difference between the old coord and new
const diffX = Math.abs(newPos.x - closestX);
const diffY = Math.abs(newPos.y - closestY);
// if the difference is less than the scaled step, we want to snap
const didSnapX = diffX < scaledStep;
const didSnapY = diffY < scaledStep;
// We may not change anything, stash the old position
let newCoordinate = { ...oldPos };
// Set the new coords based on what snapped
if (didSnapX && !didSnapY) {
newCoordinate = {
x: closestX,
y: oldPos.y,
}; };
} else if (!didSnapX && didSnapY) {
newCoordinate = {
x: oldPos.x,
y: closestY,
};
} else if (didSnapX && didSnapY) {
newCoordinate = {
x: closestX,
y: closestY,
};
}
return newCoordinate;
}, },
[scaledStep] [scaledStep]
); );
@ -272,7 +255,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
// On the Inpainting canvas, the bounding box needs to stay in the stage // On the Inpainting canvas, the bounding box needs to stay in the stage
if ( if (
activeTabName === 'inpainting' && shouldLockToInitialImage &&
(newBoundBox.width + newBoundBox.x > stageDimensions.width || (newBoundBox.width + newBoundBox.x > stageDimensions.width ||
newBoundBox.height + newBoundBox.y > stageDimensions.height || newBoundBox.height + newBoundBox.y > stageDimensions.height ||
newBoundBox.x < 0 || newBoundBox.x < 0 ||
@ -283,7 +266,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
return newBoundBox; return newBoundBox;
}, },
[activeTabName, stageDimensions.height, stageDimensions.width] [shouldLockToInitialImage, stageDimensions.height, stageDimensions.width]
); );
const handleStartedTransforming = () => { const handleStartedTransforming = () => {
@ -337,7 +320,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
globalCompositeOperation={'destination-out'} globalCompositeOperation={'destination-out'}
/> />
<Rect <Rect
{...(activeTabName === 'inpainting' ? { dragBoundFunc } : {})} {...(shouldLockToInitialImage ? { dragBoundFunc } : {})}
listening={!isDrawing && tool === 'move'} listening={!isDrawing && tool === 'move'}
draggable={true} draggable={true}
fillEnabled={false} fillEnabled={false}

View File

@ -2,17 +2,13 @@ import { GroupConfig } from 'konva/lib/Group';
import { Group, Line } from 'react-konva'; import { Group, Line } from 'react-konva';
import { useAppSelector } from 'app/store'; import { useAppSelector } from 'app/store';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { import { currentCanvasSelector, isCanvasMaskLine } from './canvasSlice';
canvasClipSelector,
currentCanvasSelector,
isCanvasMaskLine,
} from './canvasSlice';
import _ from 'lodash'; import _ from 'lodash';
export const canvasLinesSelector = createSelector( export const canvasLinesSelector = createSelector(
[currentCanvasSelector, canvasClipSelector], [currentCanvasSelector],
(currentCanvas, canvasClip) => { (currentCanvas) => {
return { objects: currentCanvas.layerState.objects, canvasClip }; return { objects: currentCanvas.layerState.objects };
}, },
{ {
memoizeOptions: { memoizeOptions: {
@ -30,10 +26,10 @@ type InpaintingCanvasLinesProps = GroupConfig;
*/ */
const IAICanvasLines = (props: InpaintingCanvasLinesProps) => { const IAICanvasLines = (props: InpaintingCanvasLinesProps) => {
const { ...rest } = props; const { ...rest } = props;
const { objects, canvasClip } = useAppSelector(canvasLinesSelector); const { objects } = useAppSelector(canvasLinesSelector);
return ( return (
<Group listening={false} {...rest} {...canvasClip}> <Group listening={false} {...rest}>
{objects.filter(isCanvasMaskLine).map((line, i) => ( {objects.filter(isCanvasMaskLine).map((line, i) => (
<Line <Line
key={i} key={i}

View File

@ -4,8 +4,8 @@ import _ from 'lodash';
import { Group, Line } from 'react-konva'; import { Group, Line } from 'react-konva';
import { import {
baseCanvasImageSelector, baseCanvasImageSelector,
canvasClipSelector, // canvasClipSelector,
canvasModeSelector, // canvasModeSelector,
currentCanvasSelector, currentCanvasSelector,
isCanvasBaseImage, isCanvasBaseImage,
isCanvasBaseLine, isCanvasBaseLine,
@ -14,13 +14,12 @@ import IAICanvasImage from './IAICanvasImage';
import { rgbaColorToString } from './util/colorToString'; import { rgbaColorToString } from './util/colorToString';
const selector = createSelector( const selector = createSelector(
[currentCanvasSelector, canvasClipSelector], [currentCanvasSelector],
(currentCanvas, canvasClip) => { (currentCanvas) => {
const { objects } = currentCanvas.layerState; const { objects } = currentCanvas.layerState;
return { return {
objects, objects,
canvasClip,
}; };
}, },
{ {
@ -31,12 +30,12 @@ const selector = createSelector(
); );
const IAICanvasObjectRenderer = () => { const IAICanvasObjectRenderer = () => {
const { objects, canvasClip } = useAppSelector(selector); const { objects } = useAppSelector(selector);
if (!objects) return null; if (!objects) return null;
return ( return (
<Group name="outpainting-objects" listening={false} {...canvasClip}> <Group name="outpainting-objects" listening={false}>
{objects.map((obj, i) => { {objects.map((obj, i) => {
if (isCanvasBaseImage(obj)) { if (isCanvasBaseImage(obj)) {
return ( return (

View File

@ -2,10 +2,11 @@ import { ButtonGroup } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { import {
currentCanvasSelector, currentCanvasSelector,
resizeAndScaleCanvas,
isStagingSelector, isStagingSelector,
resetCanvas, resetCanvas,
resetCanvasView, resetCanvasView,
setCanvasMode, setShouldLockToInitialImage,
setTool, setTool,
} from './canvasSlice'; } from './canvasSlice';
import { RootState, useAppDispatch, useAppSelector } from 'app/store'; import { RootState, useAppDispatch, useAppSelector } from 'app/store';
@ -30,6 +31,7 @@ import IAICanvasBrushButtonPopover from './IAICanvasBrushButtonPopover';
import IAICanvasMaskButtonPopover from './IAICanvasMaskButtonPopover'; import IAICanvasMaskButtonPopover from './IAICanvasMaskButtonPopover';
import { mergeAndUploadCanvas } from './util/mergeAndUploadCanvas'; import { mergeAndUploadCanvas } from './util/mergeAndUploadCanvas';
import IAICheckbox from 'common/components/IAICheckbox'; import IAICheckbox from 'common/components/IAICheckbox';
import { ChangeEvent } from 'react';
export const canvasControlsSelector = createSelector( export const canvasControlsSelector = createSelector(
[ [
@ -38,12 +40,12 @@ export const canvasControlsSelector = createSelector(
isStagingSelector, isStagingSelector,
], ],
(canvas, currentCanvas, isStaging) => { (canvas, currentCanvas, isStaging) => {
const { shouldLockToInitialImage } = canvas;
const { tool } = currentCanvas; const { tool } = currentCanvas;
const { mode } = canvas;
return { return {
tool, tool,
isStaging, isStaging,
mode, shouldLockToInitialImage,
}; };
}, },
{ {
@ -55,7 +57,16 @@ export const canvasControlsSelector = createSelector(
const IAICanvasOutpaintingControls = () => { const IAICanvasOutpaintingControls = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { tool, isStaging, mode } = useAppSelector(canvasControlsSelector); const { tool, isStaging, shouldLockToInitialImage } = useAppSelector(
canvasControlsSelector
);
const handleToggleShouldLockToInitialImage = (
e: ChangeEvent<HTMLInputElement>
) => {
dispatch(setShouldLockToInitialImage(e.target.checked));
dispatch(resizeAndScaleCanvas());
};
return ( return (
<div className="inpainting-settings"> <div className="inpainting-settings">
@ -125,10 +136,12 @@ const IAICanvasOutpaintingControls = () => {
icon={<FaCrosshairs />} icon={<FaCrosshairs />}
onClick={() => { onClick={() => {
if (!stageRef.current || !canvasImageLayerRef.current) return; if (!stageRef.current || !canvasImageLayerRef.current) return;
const clientRect = canvasImageLayerRef.current.getClientRect({skipTransform: true}); const clientRect = canvasImageLayerRef.current.getClientRect({
skipTransform: true,
});
dispatch( dispatch(
resetCanvasView({ resetCanvasView({
clientRect, contentRect: clientRect,
}) })
); );
}} }}
@ -141,13 +154,9 @@ const IAICanvasOutpaintingControls = () => {
/> />
</ButtonGroup> </ButtonGroup>
<IAICheckbox <IAICheckbox
label={'inpainting'} label={'Lock Canvas to Initial Image'}
isChecked={mode === 'inpainting'} isChecked={shouldLockToInitialImage}
onChange={(e) => onChange={handleToggleShouldLockToInitialImage}
dispatch(
setCanvasMode(e.target.checked ? 'inpainting' : 'outpainting')
)
}
/> />
</div> </div>
); );

View File

@ -5,8 +5,9 @@ import { activeTabNameSelector } from 'features/options/optionsSelectors';
import { import {
baseCanvasImageSelector, baseCanvasImageSelector,
currentCanvasSelector, currentCanvasSelector,
initializeCanvas, resizeAndScaleCanvas,
resizeCanvas, resizeCanvas,
setCanvasContainerDimensions,
setDoesCanvasNeedScaling, setDoesCanvasNeedScaling,
} from 'features/canvas/canvasSlice'; } from 'features/canvas/canvasSlice';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
@ -17,10 +18,14 @@ const canvasResizerSelector = createSelector(
baseCanvasImageSelector, baseCanvasImageSelector,
activeTabNameSelector, activeTabNameSelector,
(canvas, currentCanvas, baseCanvasImage, activeTabName) => { (canvas, currentCanvas, baseCanvasImage, activeTabName) => {
const { doesCanvasNeedScaling, mode, isCanvasInitialized } = canvas; const {
doesCanvasNeedScaling,
shouldLockToInitialImage,
isCanvasInitialized,
} = canvas;
return { return {
doesCanvasNeedScaling, doesCanvasNeedScaling,
mode, shouldLockToInitialImage,
activeTabName, activeTabName,
baseCanvasImage, baseCanvasImage,
isCanvasInitialized, isCanvasInitialized,
@ -32,7 +37,7 @@ const IAICanvasResizer = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { const {
doesCanvasNeedScaling, doesCanvasNeedScaling,
mode, shouldLockToInitialImage,
activeTabName, activeTabName,
baseCanvasImage, baseCanvasImage,
isCanvasInitialized, isCanvasInitialized,
@ -50,22 +55,17 @@ const IAICanvasResizer = () => {
const { width: imageWidth, height: imageHeight } = baseCanvasImage.image; const { width: imageWidth, height: imageHeight } = baseCanvasImage.image;
if (!isCanvasInitialized) {
dispatch( dispatch(
initializeCanvas({ setCanvasContainerDimensions({
clientWidth, width: clientWidth,
clientHeight, height: clientHeight,
imageWidth,
imageHeight,
}) })
); );
if (!isCanvasInitialized || shouldLockToInitialImage) {
dispatch(resizeAndScaleCanvas());
} else { } else {
dispatch( dispatch(resizeCanvas());
resizeCanvas({
clientWidth,
clientHeight,
})
);
} }
dispatch(setDoesCanvasNeedScaling(false)); dispatch(setDoesCanvasNeedScaling(false));

View File

@ -106,7 +106,7 @@ const IAICanvasStagingArea = (props: Props) => {
/> />
</Group> </Group>
)} )}
<Html> {/* <Html>
<CacheProvider value={emotionCache}> <CacheProvider value={emotionCache}>
<ChakraProvider> <ChakraProvider>
<div <div
@ -172,7 +172,7 @@ const IAICanvasStagingArea = (props: Props) => {
</div> </div>
</ChakraProvider> </ChakraProvider>
</CacheProvider> </CacheProvider>
</Html> </Html> */}
</Group> </Group>
); );
}; };

View File

@ -0,0 +1,140 @@
import {
background,
ButtonGroup,
ChakraProvider,
Flex,
} from '@chakra-ui/react';
import { CacheProvider } from '@emotion/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store';
import IAIButton from 'common/components/IAIButton';
import IAIIconButton from 'common/components/IAIIconButton';
import { GroupConfig } from 'konva/lib/Group';
import _ from 'lodash';
import { emotionCache } from 'main';
import { useCallback, useState } from 'react';
import {
FaArrowLeft,
FaArrowRight,
FaCheck,
FaEye,
FaEyeSlash,
FaTrash,
} from 'react-icons/fa';
import { Group, Rect } from 'react-konva';
import { Html } from 'react-konva-utils';
import {
commitStagingAreaImage,
currentCanvasSelector,
discardStagedImages,
nextStagingAreaImage,
prevStagingAreaImage,
} from './canvasSlice';
import IAICanvasImage from './IAICanvasImage';
const selector = createSelector(
[currentCanvasSelector],
(currentCanvas) => {
const {
layerState: {
stagingArea: { images, selectedImageIndex },
},
} = currentCanvas;
return {
currentStagingAreaImage:
images.length > 0 ? images[selectedImageIndex] : undefined,
isOnFirstImage: selectedImageIndex === 0,
isOnLastImage: selectedImageIndex === images.length - 1,
};
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);
const IAICanvasStagingAreaToolbar = () => {
const dispatch = useAppDispatch();
const { isOnFirstImage, isOnLastImage, currentStagingAreaImage } =
useAppSelector(selector);
const [shouldShowStagedImage, setShouldShowStagedImage] =
useState<boolean>(true);
const [shouldShowStagingAreaOutline, setShouldShowStagingAreaOutline] =
useState<boolean>(true);
const handleMouseOver = useCallback(() => {
setShouldShowStagingAreaOutline(false);
}, []);
const handleMouseOut = useCallback(() => {
setShouldShowStagingAreaOutline(true);
}, []);
if (!currentStagingAreaImage) return null;
return (
<Flex
pos={'absolute'}
bottom={'1rem'}
w={'100%'}
align={'center'}
justify={'center'}
>
<ButtonGroup isAttached>
<IAIIconButton
tooltip="Previous"
tooltipProps={{ placement: 'bottom' }}
aria-label="Previous"
icon={<FaArrowLeft />}
onClick={() => dispatch(prevStagingAreaImage())}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
data-selected={true}
isDisabled={isOnFirstImage}
/>
<IAIIconButton
tooltip="Next"
tooltipProps={{ placement: 'bottom' }}
aria-label="Next"
icon={<FaArrowRight />}
onClick={() => dispatch(nextStagingAreaImage())}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
data-selected={true}
isDisabled={isOnLastImage}
/>
<IAIIconButton
tooltip="Accept"
tooltipProps={{ placement: 'bottom' }}
aria-label="Accept"
icon={<FaCheck />}
onClick={() => dispatch(commitStagingAreaImage())}
data-selected={true}
/>
<IAIIconButton
tooltip="Show/Hide"
tooltipProps={{ placement: 'bottom' }}
aria-label="Show/Hide"
data-alert={!shouldShowStagedImage}
icon={shouldShowStagedImage ? <FaEye /> : <FaEyeSlash />}
onClick={() => setShouldShowStagedImage(!shouldShowStagedImage)}
data-selected={true}
/>
<IAIIconButton
tooltip="Discard All"
tooltipProps={{ placement: 'bottom' }}
aria-label="Discard All"
icon={<FaTrash />}
onClick={() => dispatch(discardStagedImages())}
data-selected={true}
/>
</ButtonGroup>
</Flex>
);
};
export default IAICanvasStagingAreaToolbar;

View File

@ -21,6 +21,7 @@ import { uploadImage } from 'features/gallery/util/uploadImage';
import { setInitialCanvasImage } from './canvasReducers'; import { setInitialCanvasImage } from './canvasReducers';
import calculateScale from './util/calculateScale'; import calculateScale from './util/calculateScale';
import calculateCoordinates from './util/calculateCoordinates'; import calculateCoordinates from './util/calculateCoordinates';
import floorCoordinates from './util/floorCoordinates';
export interface GenericCanvasState { export interface GenericCanvasState {
boundingBoxCoordinates: Vector2d; boundingBoxCoordinates: Vector2d;
@ -58,6 +59,7 @@ export interface GenericCanvasState {
stageDimensions: Dimensions; stageDimensions: Dimensions;
stageScale: number; stageScale: number;
tool: CanvasTool; tool: CanvasTool;
minimumStageScale: number;
} }
export type CanvasMode = 'inpainting' | 'outpainting'; export type CanvasMode = 'inpainting' | 'outpainting';
@ -147,8 +149,10 @@ export interface CanvasState {
currentCanvas: ValidCanvasName; currentCanvas: ValidCanvasName;
inpainting: InpaintingCanvasState; inpainting: InpaintingCanvasState;
outpainting: OutpaintingCanvasState; outpainting: OutpaintingCanvasState;
mode: CanvasMode; // mode: CanvasMode;
shouldLockToInitialImage: boolean;
isCanvasInitialized: boolean; isCanvasInitialized: boolean;
canvasContainerDimensions: Dimensions;
} }
export const initialLayerState: CanvasLayerState = { export const initialLayerState: CanvasLayerState = {
@ -197,14 +201,17 @@ const initialGenericCanvasState: GenericCanvasState = {
stageCoordinates: { x: 0, y: 0 }, stageCoordinates: { x: 0, y: 0 },
stageDimensions: { width: 0, height: 0 }, stageDimensions: { width: 0, height: 0 },
stageScale: 1, stageScale: 1,
minimumStageScale: 1,
tool: 'brush', tool: 'brush',
}; };
const initialCanvasState: CanvasState = { const initialCanvasState: CanvasState = {
currentCanvas: 'inpainting', currentCanvas: 'inpainting',
doesCanvasNeedScaling: false, doesCanvasNeedScaling: false,
mode: 'outpainting', shouldLockToInitialImage: false,
// mode: 'outpainting',
isCanvasInitialized: false, isCanvasInitialized: false,
canvasContainerDimensions: { width: 0, height: 0 },
inpainting: { inpainting: {
layer: 'mask', layer: 'mask',
...initialGenericCanvasState, ...initialGenericCanvasState,
@ -335,80 +342,15 @@ export const canvasSlice = createSlice({
}; };
}, },
setBoundingBoxDimensions: (state, action: PayloadAction<Dimensions>) => { setBoundingBoxDimensions: (state, action: PayloadAction<Dimensions>) => {
const currentCanvas = state[state.currentCanvas]; state[state.currentCanvas].boundingBoxDimensions = action.payload;
currentCanvas.boundingBoxDimensions = action.payload;
const { width: boundingBoxWidth, height: boundingBoxHeight } =
action.payload;
const { x: boundingBoxX, y: boundingBoxY } =
currentCanvas.boundingBoxCoordinates;
const { width: canvasWidth, height: canvasHeight } =
currentCanvas.stageDimensions;
const scaledCanvasWidth = canvasWidth / currentCanvas.stageScale;
const scaledCanvasHeight = canvasHeight / currentCanvas.stageScale;
const roundedCanvasWidth = roundDownToMultiple(scaledCanvasWidth, 64);
const roundedCanvasHeight = roundDownToMultiple(scaledCanvasHeight, 64);
const roundedBoundingBoxWidth = roundDownToMultiple(boundingBoxWidth, 64);
const roundedBoundingBoxHeight = roundDownToMultiple(
boundingBoxHeight,
64
);
const overflowX = boundingBoxX + boundingBoxWidth - scaledCanvasWidth;
const overflowY = boundingBoxY + boundingBoxHeight - scaledCanvasHeight;
const newBoundingBoxWidth = _.clamp(
roundedBoundingBoxWidth,
64,
roundedCanvasWidth
);
const newBoundingBoxHeight = _.clamp(
roundedBoundingBoxHeight,
64,
roundedCanvasHeight
);
const overflowCorrectedX =
overflowX > 0 ? boundingBoxX - overflowX : boundingBoxX;
const overflowCorrectedY =
overflowY > 0 ? boundingBoxY - overflowY : boundingBoxY;
const clampedX = _.clamp(
overflowCorrectedX,
currentCanvas.stageCoordinates.x,
roundedCanvasWidth - newBoundingBoxWidth
);
const clampedY = _.clamp(
overflowCorrectedY,
currentCanvas.stageCoordinates.y,
roundedCanvasHeight - newBoundingBoxHeight
);
currentCanvas.boundingBoxDimensions = {
width: newBoundingBoxWidth,
height: newBoundingBoxHeight,
};
currentCanvas.boundingBoxCoordinates = {
x: clampedX,
y: clampedY,
};
}, },
setBoundingBoxCoordinates: (state, action: PayloadAction<Vector2d>) => { setBoundingBoxCoordinates: (state, action: PayloadAction<Vector2d>) => {
const { x, y } = action.payload; state[state.currentCanvas].boundingBoxCoordinates = floorCoordinates(
state[state.currentCanvas].boundingBoxCoordinates = { action.payload
x: Math.floor(x), );
y: Math.floor(y),
};
}, },
setStageCoordinates: (state, action: PayloadAction<Vector2d>) => { setStageCoordinates: (state, action: PayloadAction<Vector2d>) => {
state[state.currentCanvas].stageCoordinates = action.payload; state.outpainting.stageCoordinates = floorCoordinates(action.payload);
}, },
setBoundingBoxPreviewFill: (state, action: PayloadAction<RgbaColor>) => { setBoundingBoxPreviewFill: (state, action: PayloadAction<RgbaColor>) => {
state[state.currentCanvas].boundingBoxPreviewFill = action.payload; state[state.currentCanvas].boundingBoxPreviewFill = action.payload;
@ -595,30 +537,52 @@ export const canvasSlice = createSlice({
state[state.currentCanvas].layerState = initialLayerState; state[state.currentCanvas].layerState = initialLayerState;
state[state.currentCanvas].futureLayerStates = []; state[state.currentCanvas].futureLayerStates = [];
}, },
initializeCanvas: ( setCanvasContainerDimensions: (
state, state,
action: PayloadAction<{ action: PayloadAction<Dimensions>
clientWidth: number;
clientHeight: number;
imageWidth: number;
imageHeight: number;
}>
) => { ) => {
const { clientWidth, clientHeight, imageWidth, imageHeight } = state.canvasContainerDimensions = action.payload;
action.payload; },
resizeAndScaleCanvas: (state) => {
const { width: containerWidth, height: containerHeight } =
state.canvasContainerDimensions;
const initialCanvasImage =
state.outpainting.layerState.objects.find(isCanvasBaseImage);
if (!initialCanvasImage) return;
const { width: imageWidth, height: imageHeight } = initialCanvasImage;
// const { clientWidth, clientHeight, imageWidth, imageHeight } =
// action.payload;
const { shouldLockToInitialImage } = state;
const currentCanvas = state[state.currentCanvas]; const currentCanvas = state[state.currentCanvas];
const padding = shouldLockToInitialImage ? 1 : 0.95;
const newScale = calculateScale( const newScale = calculateScale(
clientWidth, containerWidth,
clientHeight, containerHeight,
imageWidth, imageWidth,
imageHeight imageHeight,
padding
); );
const newDimensions = {
width: shouldLockToInitialImage
? Math.floor(imageWidth * newScale)
: Math.floor(containerWidth),
height: shouldLockToInitialImage
? Math.floor(imageHeight * newScale)
: Math.floor(containerHeight),
};
const newCoordinates = calculateCoordinates( const newCoordinates = calculateCoordinates(
clientWidth, newDimensions.width,
clientHeight, newDimensions.height,
0, 0,
0, 0,
imageWidth, imageWidth,
@ -627,50 +591,53 @@ export const canvasSlice = createSlice({
); );
currentCanvas.stageScale = newScale; currentCanvas.stageScale = newScale;
currentCanvas.minimumStageScale = newScale;
currentCanvas.stageCoordinates = newCoordinates; currentCanvas.stageCoordinates = newCoordinates;
currentCanvas.stageDimensions = { currentCanvas.stageDimensions = newDimensions;
width: Math.floor(clientWidth),
height: Math.floor(clientHeight),
};
state.isCanvasInitialized = true; state.isCanvasInitialized = true;
}, },
resizeCanvas: ( resizeCanvas: (state) => {
state, const { width: containerWidth, height: containerHeight } =
action: PayloadAction<{ state.canvasContainerDimensions;
clientWidth: number;
clientHeight: number;
}>
) => {
const { clientWidth, clientHeight } = action.payload;
const currentCanvas = state[state.currentCanvas]; const currentCanvas = state[state.currentCanvas];
currentCanvas.stageDimensions = { currentCanvas.stageDimensions = {
width: Math.floor(clientWidth), width: Math.floor(containerWidth),
height: Math.floor(clientHeight), height: Math.floor(containerHeight),
}; };
}, },
resetCanvasView: ( resetCanvasView: (
state, state,
action: PayloadAction<{ action: PayloadAction<{
clientRect: IRect; contentRect: IRect;
}> }>
) => { ) => {
const { clientRect } = action.payload; const { contentRect } = action.payload;
const currentCanvas = state[state.currentCanvas]; const currentCanvas = state[state.currentCanvas];
const baseCanvasImage = const baseCanvasImage =
currentCanvas.layerState.objects.find(isCanvasBaseImage); currentCanvas.layerState.objects.find(isCanvasBaseImage);
const { shouldLockToInitialImage } = state;
if (!baseCanvasImage) return; if (!baseCanvasImage) return;
const { const {
stageDimensions: { width: stageWidth, height: stageHeight }, stageDimensions: { width: stageWidth, height: stageHeight },
} = currentCanvas; } = currentCanvas;
const { x, y, width, height } = clientRect; const { x, y, width, height } = contentRect;
const padding = shouldLockToInitialImage ? 1 : 0.95;
const newScale = calculateScale(
stageWidth,
stageHeight,
width,
height,
padding
);
const newScale = calculateScale(stageWidth, stageHeight, width, height);
const newCoordinates = calculateCoordinates( const newCoordinates = calculateCoordinates(
stageWidth, stageWidth,
stageHeight, stageHeight,
@ -683,10 +650,7 @@ export const canvasSlice = createSlice({
currentCanvas.stageScale = newScale; currentCanvas.stageScale = newScale;
currentCanvas.stageCoordinates = { currentCanvas.stageCoordinates = newCoordinates;
x: stageWidth / 2 - (x + width / 2) * newScale,
y: stageHeight / 2 - (y + height / 2) * newScale,
};
}, },
nextStagingAreaImage: (state) => { nextStagingAreaImage: (state) => {
const currentIndex = const currentIndex =
@ -728,14 +692,17 @@ export const canvasSlice = createSlice({
currentCanvas.futureLayerStates = []; currentCanvas.futureLayerStates = [];
}, },
setCanvasMode: (state, action: PayloadAction<CanvasMode>) => { setShouldLockToInitialImage: (state, action: PayloadAction<boolean>) => {
state.mode = action.payload; state.shouldLockToInitialImage = action.payload;
}, },
// setCanvasMode: (state, action: PayloadAction<CanvasMode>) => {
// state.mode = action.payload;
// },
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(mergeAndUploadCanvas.fulfilled, (state, action) => { builder.addCase(mergeAndUploadCanvas.fulfilled, (state, action) => {
if (!action.payload) return; if (!action.payload) return;
const { image, kind, boundingBox } = action.payload; const { image, kind, originalBoundingBox } = action.payload;
if (kind === 'temp_merged_canvas') { if (kind === 'temp_merged_canvas') {
state.outpainting.pastLayerStates.push({ state.outpainting.pastLayerStates.push({
@ -748,7 +715,7 @@ export const canvasSlice = createSlice({
{ {
kind: 'image', kind: 'image',
layer: 'base', layer: 'base',
...boundingBox, ...originalBoundingBox,
image, image,
}, },
]; ];
@ -824,10 +791,11 @@ export const {
prevStagingAreaImage, prevStagingAreaImage,
commitStagingAreaImage, commitStagingAreaImage,
discardStagedImages, discardStagedImages,
setCanvasMode, setShouldLockToInitialImage,
initializeCanvas, resizeAndScaleCanvas,
resizeCanvas, resizeCanvas,
resetCanvasView, resetCanvasView,
setCanvasContainerDimensions,
} = canvasSlice.actions; } = canvasSlice.actions;
export default canvasSlice.reducer; export default canvasSlice.reducer;
@ -847,8 +815,8 @@ export const inpaintingCanvasSelector = (
state: RootState state: RootState
): InpaintingCanvasState => state.canvas.inpainting; ): InpaintingCanvasState => state.canvas.inpainting;
export const canvasModeSelector = (state: RootState): CanvasMode => export const shouldLockToInitialImageSelector = (state: RootState): boolean =>
state.canvas.mode; state.canvas.shouldLockToInitialImage;
export const baseCanvasImageSelector = createSelector( export const baseCanvasImageSelector = createSelector(
[currentCanvasSelector], [currentCanvasSelector],
@ -857,16 +825,16 @@ export const baseCanvasImageSelector = createSelector(
} }
); );
export const canvasClipSelector = createSelector( // export const canvasClipSelector = createSelector(
[canvasModeSelector, baseCanvasImageSelector], // [canvasModeSelector, baseCanvasImageSelector],
(canvasMode, baseCanvasImage) => { // (canvasMode, baseCanvasImage) => {
return canvasMode === 'inpainting' // return canvasMode === 'inpainting'
? { // ? {
clipX: 0, // clipX: 0,
clipY: 0, // clipY: 0,
clipWidth: baseCanvasImage?.width, // clipWidth: baseCanvasImage?.width,
clipHeight: baseCanvasImage?.height, // clipHeight: baseCanvasImage?.height,
} // }
: {}; // : {};
} // }
); // );

View File

@ -5,20 +5,21 @@ import { KonvaEventObject } from 'konva/lib/Node';
import _ from 'lodash'; import _ from 'lodash';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { import {
baseCanvasImageSelector,
currentCanvasSelector, currentCanvasSelector,
isStagingSelector, isStagingSelector,
setIsMovingStage, setIsMovingStage,
setStageCoordinates, setStageCoordinates,
shouldLockToInitialImageSelector,
} from '../canvasSlice'; } from '../canvasSlice';
const selector = createSelector( const selector = createSelector(
[currentCanvasSelector, isStagingSelector, activeTabNameSelector], [currentCanvasSelector, isStagingSelector],
(canvas, isStaging, activeTabName) => { (canvas, isStaging) => {
const { tool } = canvas; const { tool } = canvas;
return { return {
tool, tool,
isStaging, isStaging,
activeTabName,
}; };
}, },
{ memoizeOptions: { resultEqualityCheck: _.isEqual } } { memoizeOptions: { resultEqualityCheck: _.isEqual } }
@ -26,7 +27,7 @@ const selector = createSelector(
const useCanvasDrag = () => { const useCanvasDrag = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { tool, activeTabName, isStaging } = useAppSelector(selector); const { tool, isStaging } = useAppSelector(selector);
return { return {
handleDragStart: useCallback(() => { handleDragStart: useCallback(() => {
@ -37,7 +38,10 @@ const useCanvasDrag = () => {
handleDragMove: useCallback( handleDragMove: useCallback(
(e: KonvaEventObject<MouseEvent>) => { (e: KonvaEventObject<MouseEvent>) => {
if (!(tool === 'move' || isStaging)) return; if (!(tool === 'move' || isStaging)) return;
dispatch(setStageCoordinates(e.target.getPosition()));
const newCoordinates = { x: e.target.x(), y: e.target.y() };
dispatch(setStageCoordinates(newCoordinates));
}, },
[dispatch, isStaging, tool] [dispatch, isStaging, tool]
), ),

View File

@ -1,15 +1,17 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store'; import { RootState, useAppDispatch, useAppSelector } from 'app/store';
import { activeTabNameSelector } from 'features/options/optionsSelectors'; import { activeTabNameSelector } from 'features/options/optionsSelectors';
import Konva from 'konva'; import Konva from 'konva';
import { KonvaEventObject } from 'konva/lib/Node'; import { KonvaEventObject } from 'konva/lib/Node';
import _ from 'lodash'; import _ from 'lodash';
import { MutableRefObject, useCallback } from 'react'; import { MutableRefObject, useCallback } from 'react';
import { import {
baseCanvasImageSelector,
currentCanvasSelector, currentCanvasSelector,
GenericCanvasState, GenericCanvasState,
setStageCoordinates, setStageCoordinates,
setStageScale, setStageScale,
shouldLockToInitialImageSelector,
} from '../canvasSlice'; } from '../canvasSlice';
import { import {
CANVAS_SCALE_BY, CANVAS_SCALE_BY,
@ -18,13 +20,34 @@ import {
} from '../util/constants'; } from '../util/constants';
const selector = createSelector( const selector = createSelector(
[activeTabNameSelector, currentCanvasSelector], [
(activeTabName, canvas: GenericCanvasState) => { (state: RootState) => state.canvas,
const { isMoveStageKeyHeld, stageScale } = canvas; activeTabNameSelector,
currentCanvasSelector,
baseCanvasImageSelector,
shouldLockToInitialImageSelector,
],
(
canvas,
activeTabName,
currentCanvas,
baseCanvasImage,
shouldLockToInitialImage
) => {
const {
isMoveStageKeyHeld,
stageScale,
stageDimensions,
minimumStageScale,
} = currentCanvas;
return { return {
isMoveStageKeyHeld, isMoveStageKeyHeld,
stageScale, stageScale,
activeTabName, activeTabName,
baseCanvasImage,
shouldLockToInitialImage,
stageDimensions,
minimumStageScale,
}; };
}, },
{ memoizeOptions: { resultEqualityCheck: _.isEqual } } { memoizeOptions: { resultEqualityCheck: _.isEqual } }
@ -32,19 +55,29 @@ const selector = createSelector(
const useCanvasWheel = (stageRef: MutableRefObject<Konva.Stage | null>) => { const useCanvasWheel = (stageRef: MutableRefObject<Konva.Stage | null>) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { isMoveStageKeyHeld, stageScale, activeTabName } = const {
useAppSelector(selector); isMoveStageKeyHeld,
stageScale,
activeTabName,
baseCanvasImage,
shouldLockToInitialImage,
stageDimensions,
minimumStageScale,
} = useAppSelector(selector);
return useCallback( return useCallback(
(e: KonvaEventObject<WheelEvent>) => { (e: KonvaEventObject<WheelEvent>) => {
// stop default scrolling // stop default scrolling
if (activeTabName !== 'outpainting') return; if (
activeTabName !== 'outpainting' ||
!stageRef.current ||
isMoveStageKeyHeld ||
!baseCanvasImage
)
return;
e.evt.preventDefault(); e.evt.preventDefault();
// const oldScale = stageRef.current.scaleX();
if (!stageRef.current || isMoveStageKeyHeld) return;
const cursorPos = stageRef.current.getPointerPosition(); const cursorPos = stageRef.current.getPointerPosition();
if (!cursorPos) return; if (!cursorPos) return;
@ -64,19 +97,44 @@ const useCanvasWheel = (stageRef: MutableRefObject<Konva.Stage | null>) => {
const newScale = _.clamp( const newScale = _.clamp(
stageScale * CANVAS_SCALE_BY ** delta, stageScale * CANVAS_SCALE_BY ** delta,
MIN_CANVAS_SCALE, shouldLockToInitialImage ? minimumStageScale : MIN_CANVAS_SCALE,
MAX_CANVAS_SCALE MAX_CANVAS_SCALE
); );
const newPos = { const newCoordinates = {
x: cursorPos.x - mousePointTo.x * newScale, x: cursorPos.x - mousePointTo.x * newScale,
y: cursorPos.y - mousePointTo.y * newScale, y: cursorPos.y - mousePointTo.y * newScale,
}; };
if (shouldLockToInitialImage) {
newCoordinates.x = _.clamp(
newCoordinates.x,
stageDimensions.width - Math.floor(baseCanvasImage.width * newScale),
0
);
newCoordinates.y = _.clamp(
newCoordinates.y,
stageDimensions.height -
Math.floor(baseCanvasImage.height * newScale),
0
);
}
dispatch(setStageScale(newScale)); dispatch(setStageScale(newScale));
dispatch(setStageCoordinates(newPos)); dispatch(setStageCoordinates(newCoordinates));
}, },
[activeTabName, dispatch, isMoveStageKeyHeld, stageRef, stageScale] [
activeTabName,
stageRef,
isMoveStageKeyHeld,
baseCanvasImage,
stageScale,
shouldLockToInitialImage,
minimumStageScale,
dispatch,
stageDimensions.width,
stageDimensions.height,
]
); );
}; };

View File

@ -0,0 +1,10 @@
import { Vector2d } from 'konva/lib/types';
const floorCoordinates = (coord: Vector2d): Vector2d => {
return {
x: Math.floor(coord.x),
y: Math.floor(coord.y),
};
};
export default floorCoordinates;

View File

@ -3,7 +3,7 @@ import Konva from 'konva';
const layerToDataURL = (layer: Konva.Layer, stageScale: number) => { const layerToDataURL = (layer: Konva.Layer, stageScale: number) => {
const tempScale = layer.scale(); const tempScale = layer.scale();
const { x: relativeX, y: relativeY } = layer.getClientRect({ const relativeClientRect = layer.getClientRect({
relativeTo: layer.getParent(), relativeTo: layer.getParent(),
}); });
@ -13,14 +13,27 @@ const layerToDataURL = (layer: Konva.Layer, stageScale: number) => {
y: 1 / stageScale, y: 1 / stageScale,
}); });
const clientRect = layer.getClientRect(); const { x, y, width, height } = layer.getClientRect();
const dataURL = layer.toDataURL(clientRect); const dataURL = layer.toDataURL({
x: Math.round(x),
y: Math.round(y),
width: Math.round(width),
height: Math.round(height),
});
// Unscale the canvas // Unscale the canvas
layer.scale(tempScale); layer.scale(tempScale);
return { dataURL, relativeX, relativeY }; return {
dataURL,
boundingBox: {
x: Math.round(relativeClientRect.x),
y: Math.round(relativeClientRect.y),
width: Math.round(width),
height: Math.round(height),
},
};
}; };
export default layerToDataURL; export default layerToDataURL;

View File

@ -25,7 +25,7 @@ export const mergeAndUploadCanvas = createAsyncThunk(
if (!canvasImageLayerRef.current) return; if (!canvasImageLayerRef.current) return;
const { dataURL, relativeX, relativeY } = layerToDataURL( const { dataURL, boundingBox: originalBoundingBox } = layerToDataURL(
canvasImageLayerRef.current, canvasImageLayerRef.current,
stageScale stageScale
); );
@ -34,32 +34,45 @@ export const mergeAndUploadCanvas = createAsyncThunk(
const formData = new FormData(); const formData = new FormData();
formData.append('dataURL', dataURL); formData.append(
formData.append('filename', 'merged_canvas.png'); 'data',
formData.append('kind', saveToGallery ? 'result' : 'temp'); JSON.stringify({
dataURL,
filename: 'merged_canvas.png',
kind: saveToGallery ? 'result' : 'temp',
cropVisible: saveToGallery,
})
);
const response = await fetch(window.location.origin + '/upload', { const response = await fetch(window.location.origin + '/upload', {
method: 'POST', method: 'POST',
body: formData, body: formData,
}); });
const { image } = (await response.json()) as InvokeAI.ImageUploadResponse; const { url, mtime, width, height } =
(await response.json()) as InvokeAI.ImageUploadResponse;
// const newBoundingBox = {
// x: bbox[0],
// y: bbox[1],
// width: bbox[2],
// height: bbox[3],
// };
const newImage: InvokeAI.Image = { const newImage: InvokeAI.Image = {
uuid: uuidv4(), uuid: uuidv4(),
url,
mtime,
category: saveToGallery ? 'result' : 'user', category: saveToGallery ? 'result' : 'user',
...image, width: width,
height: height,
}; };
return { return {
image: newImage, image: newImage,
kind: saveToGallery ? 'merged_canvas' : 'temp_merged_canvas', kind: saveToGallery ? 'merged_canvas' : 'temp_merged_canvas',
boundingBox: { originalBoundingBox,
x: relativeX, // newBoundingBox,
y: relativeY,
width: image.width,
height: image.height,
},
}; };
} }
); );

View File

@ -271,7 +271,7 @@ export const gallerySlice = createSlice({
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(mergeAndUploadCanvas.fulfilled, (state, action) => { builder.addCase(mergeAndUploadCanvas.fulfilled, (state, action) => {
if (!action.payload) return; if (!action.payload) return;
const { image, kind, boundingBox } = action.payload; const { image, kind, originalBoundingBox } = action.payload;
if (kind === 'merged_canvas') { if (kind === 'merged_canvas') {
const { uuid, url, mtime } = image; const { uuid, url, mtime } = image;

View File

@ -23,19 +23,36 @@ export const uploadImage = createAsyncThunk(
const formData = new FormData(); const formData = new FormData();
formData.append('file', imageFile, imageFile.name); formData.append('file', imageFile, imageFile.name);
formData.append('kind', 'init'); formData.append(
'data',
JSON.stringify({
kind: 'init',
})
);
// formData.append('kind', 'init');
const response = await fetch(window.location.origin + '/upload', { const response = await fetch(window.location.origin + '/upload', {
method: 'POST', method: 'POST',
body: formData, body: formData,
}); });
const { image } = (await response.json()) as InvokeAI.ImageUploadResponse; const { url, mtime, width, height } =
(await response.json()) as InvokeAI.ImageUploadResponse;
// const newBoundingBox = {
// x: bbox[0],
// y: bbox[1],
// width: bbox[2],
// height: bbox[3],
// };
const newImage: InvokeAI.Image = { const newImage: InvokeAI.Image = {
uuid: uuidv4(), uuid: uuidv4(),
url,
mtime,
category: 'user', category: 'user',
...image, width: width,
height: height,
}; };
return { return {