mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Initial unification of canvas
This commit is contained in:
parent
caf8f0ae35
commit
15dd1339d2
@ -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)
|
||||||
|
7
frontend/src/app/invokeai.d.ts
vendored
7
frontend/src/app/invokeai.d.ts
vendored
@ -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 = {
|
||||||
|
@ -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',
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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 (
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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));
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
140
frontend/src/features/canvas/IAICanvasStagingAreaToolbar.tsx
Normal file
140
frontend/src/features/canvas/IAICanvasStagingAreaToolbar.tsx
Normal 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;
|
@ -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,
|
||||||
}
|
// }
|
||||||
: {};
|
// : {};
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
|
@ -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]
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
10
frontend/src/features/canvas/util/floorCoordinates.ts
Normal file
10
frontend/src/features/canvas/util/floorCoordinates.ts
Normal 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;
|
@ -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;
|
||||||
|
@ -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,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user