From 425d3bc95d5fb05d16043d6a4204ab6cddb95dd6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 17 Nov 2022 19:16:53 +1100 Subject: [PATCH] Clips lines drawn while canvas locked When drawing with the locked canvas, if a brush stroke gets too close to the edge of the canvas and its stroke would extend past the edge of the canvas, the edge of that stroke will be seen after unlocking the canvas. This could cause a problem if you unlock the canvas and now have a bunch of strokes just outside the init image area, which are far back in undo history and you cannot easily erase. With this change, lines drawn while the canvas is locked get clipped to the initial image bbox, fixing this issue. Additionally, the merge and save to gallery functions have been updated to respect the initial image bbox so they function how you'd expect. --- .../components/IAICanvasObjectRenderer.tsx | 35 ++++++++++--------- .../src/features/canvas/store/canvasSlice.ts | 23 ++++++++---- .../src/features/canvas/store/canvasTypes.ts | 11 +++++- .../store/reducers/setInitialCanvasImage.ts | 8 ++++- .../features/canvas/util/layerToDataURL.ts | 28 +++++++++++---- .../canvas/util/mergeAndUploadCanvas.ts | 14 +++++++- 6 files changed, 87 insertions(+), 32 deletions(-) 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;