diff --git a/frontend/src/features/canvas/components/IAICanvasObjectRenderer.tsx b/frontend/src/features/canvas/components/IAICanvasObjectRenderer.tsx index e75c6b2882..5aa4923f03 100644 --- a/frontend/src/features/canvas/components/IAICanvasObjectRenderer.tsx +++ b/frontend/src/features/canvas/components/IAICanvasObjectRenderer.tsx @@ -10,8 +10,9 @@ import { canvasSelector } from 'features/canvas/store/canvasSelectors'; const selector = createSelector( [canvasSelector], (canvas) => { - const { objects } = canvas.layerState; - + const { + layerState: { objects }, + } = canvas; return { objects, }; @@ -37,20 +38,22 @@ const IAICanvasObjectRenderer = () => { ); } else if (isCanvasBaseLine(obj)) { return ( - 0 - strokeWidth={obj.strokeWidth * 2} - tension={0} - lineCap="round" - lineJoin="round" - shadowForStrokeEnabled={false} - listening={false} - globalCompositeOperation={ - obj.tool === 'brush' ? 'source-over' : 'destination-out' - } - /> + + 0 + strokeWidth={obj.strokeWidth * 2} + tension={0} + lineCap="round" + lineJoin="round" + shadowForStrokeEnabled={false} + listening={false} + globalCompositeOperation={ + obj.tool === 'brush' ? 'source-over' : 'destination-out' + } + /> + ); } })} diff --git a/frontend/src/features/canvas/store/canvasSlice.ts b/frontend/src/features/canvas/store/canvasSlice.ts index d93ea4788c..14fc6ad1eb 100644 --- a/frontend/src/features/canvas/store/canvasSlice.ts +++ b/frontend/src/features/canvas/store/canvasSlice.ts @@ -48,6 +48,7 @@ const initialCanvasState: CanvasState = { eraserSize: 50, futureLayerStates: [], inpaintReplace: 0.1, + initialCanvasImageClipRect: undefined, isCanvasInitialized: false, isDrawing: false, isMaskEnabled: true, @@ -296,6 +297,10 @@ export const canvasSlice = createSlice({ tool, strokeWidth: newStrokeWidth, points: action.payload, + clipRect: + state.shouldLockToInitialImage && state.initialCanvasImageClipRect + ? state.initialCanvasImageClipRect + : undefined, ...newColor, }); @@ -369,10 +374,14 @@ export const canvasSlice = createSlice({ state.layerState.objects.find(isCanvasBaseImage); if (!initialCanvasImage) return; + const { shouldLockToInitialImage, initialCanvasImageClipRect } = state; - const { width: imageWidth, height: imageHeight } = initialCanvasImage; + let { width: imageWidth, height: imageHeight } = initialCanvasImage; - const { shouldLockToInitialImage } = state; + if (shouldLockToInitialImage && initialCanvasImageClipRect) { + imageWidth = initialCanvasImageClipRect.clipWidth; + imageHeight = initialCanvasImageClipRect.clipHeight; + } const padding = shouldLockToInitialImage ? 1 : 0.95; @@ -403,11 +412,13 @@ export const canvasSlice = createSlice({ newScale ); - state.stageScale = newScale; - state.minimumStageScale = newScale; - state.stageCoordinates = newCoordinates; + if (!_.isEqual(state.stageDimensions, newDimensions)) { + state.minimumStageScale = newScale; + state.stageScale = newScale; + state.stageCoordinates = newCoordinates; + state.stageDimensions = newDimensions; + } - state.stageDimensions = newDimensions; state.isCanvasInitialized = true; }, resizeCanvas: (state) => { diff --git a/frontend/src/features/canvas/store/canvasTypes.ts b/frontend/src/features/canvas/store/canvasTypes.ts index 22e81f2677..d3255100df 100644 --- a/frontend/src/features/canvas/store/canvasTypes.ts +++ b/frontend/src/features/canvas/store/canvasTypes.ts @@ -1,5 +1,5 @@ import * as InvokeAI from 'app/invokeai'; -import { Vector2d } from 'konva/lib/types'; +import { IRect, Vector2d } from 'konva/lib/types'; import { RgbaColor } from 'react-colorful'; export type CanvasLayer = 'base' | 'mask'; @@ -8,6 +8,13 @@ export type CanvasDrawingTool = 'brush' | 'eraser'; export type CanvasTool = CanvasDrawingTool | 'move'; +export type ClipRect = { + clipX: number; + clipY: number; + clipWidth: number; + clipHeight: number; +}; + export type Dimensions = { width: number; height: number; @@ -18,6 +25,7 @@ export type CanvasAnyLine = { tool: CanvasDrawingTool; strokeWidth: number; points: number[]; + clipRect: ClipRect | undefined; }; export type CanvasImage = { @@ -78,6 +86,7 @@ export interface CanvasState { doesCanvasNeedScaling: boolean; eraserSize: number; futureLayerStates: CanvasLayerState[]; + initialCanvasImageClipRect?: ClipRect; inpaintReplace: number; intermediateImage?: InvokeAI.Image; isCanvasInitialized: boolean; diff --git a/frontend/src/features/canvas/store/reducers/setInitialCanvasImage.ts b/frontend/src/features/canvas/store/reducers/setInitialCanvasImage.ts index 527f014ed3..0be4274ca9 100644 --- a/frontend/src/features/canvas/store/reducers/setInitialCanvasImage.ts +++ b/frontend/src/features/canvas/store/reducers/setInitialCanvasImage.ts @@ -28,7 +28,6 @@ export const setInitialCanvasImage = ( }; state.boundingBoxDimensions = newBoundingBoxDimensions; - state.boundingBoxCoordinates = newBoundingBoxCoordinates; state.pastLayerStates.push(state.layerState); @@ -48,6 +47,13 @@ export const setInitialCanvasImage = ( }; state.futureLayerStates = []; + state.initialCanvasImageClipRect = { + clipX: 0, + clipY: 0, + clipWidth: image.width, + clipHeight: image.height, + }; + state.isCanvasInitialized = false; state.doesCanvasNeedScaling = true; }; diff --git a/frontend/src/features/canvas/util/layerToDataURL.ts b/frontend/src/features/canvas/util/layerToDataURL.ts index abdbc0a21d..701b2f53d9 100644 --- a/frontend/src/features/canvas/util/layerToDataURL.ts +++ b/frontend/src/features/canvas/util/layerToDataURL.ts @@ -1,6 +1,11 @@ import Konva from 'konva'; +import { IRect } from 'konva/lib/types'; -const layerToDataURL = (layer: Konva.Layer, stageScale: number) => { +const layerToDataURL = ( + layer: Konva.Layer, + stageScale: number, + boundingBox?: IRect +) => { const tempScale = layer.scale(); const relativeClientRect = layer.getClientRect({ @@ -15,12 +20,21 @@ const layerToDataURL = (layer: Konva.Layer, stageScale: number) => { const { x, y, width, height } = layer.getClientRect(); - const dataURL = layer.toDataURL({ - x: Math.round(x), - y: Math.round(y), - width: Math.round(width), - height: Math.round(height), - }); + const scaledBoundingBox = boundingBox + ? { + x: Math.round(boundingBox.x / stageScale), + y: Math.round(boundingBox.y / stageScale), + width: Math.round(boundingBox.width / stageScale), + height: Math.round(boundingBox.height / stageScale), + } + : { + x: Math.round(x), + y: Math.round(y), + width: Math.round(width), + height: Math.round(height), + }; + + const dataURL = layer.toDataURL(scaledBoundingBox); // Unscale the canvas layer.scale(tempScale); diff --git a/frontend/src/features/canvas/util/mergeAndUploadCanvas.ts b/frontend/src/features/canvas/util/mergeAndUploadCanvas.ts index 76ad6e4e41..8442722747 100644 --- a/frontend/src/features/canvas/util/mergeAndUploadCanvas.ts +++ b/frontend/src/features/canvas/util/mergeAndUploadCanvas.ts @@ -22,12 +22,24 @@ export const mergeAndUploadCanvas = createAsyncThunk( const state = getState() as RootState; const stageScale = state.canvas.stageScale; + const clipRect = state.canvas.initialCanvasImageClipRect; + + const boundingBox = + state.canvas.shouldLockToInitialImage && clipRect && saveToGallery + ? { + x: clipRect.clipX, + y: clipRect.clipY, + width: clipRect.clipWidth, + height: clipRect.clipHeight, + } + : undefined; if (!canvasImageLayerRef.current) return; const { dataURL, boundingBox: originalBoundingBox } = layerToDataURL( canvasImageLayerRef.current, - stageScale + stageScale, + boundingBox ); if (!dataURL) return;