From 4668ea449b0650c5afacf86beb87aa05d02105d3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Aug 2024 17:24:16 +1000 Subject: [PATCH] fix(ui): staging area works --- .../listeners/imageDropped.ts | 11 ++- .../components/AddLayerButton.tsx | 2 +- .../controlLayers/konva/CanvasBbox.ts | 7 +- .../controlLayers/konva/CanvasImage.ts | 89 +++++++++-------- .../controlLayers/konva/CanvasManager.ts | 59 +++--------- .../controlLayers/konva/CanvasPreview.ts | 55 ++++++----- .../konva/CanvasProgressImage.ts | 96 +++++++++++++------ .../konva/CanvasProgressPreview.ts | 46 --------- .../controlLayers/konva/CanvasStagingArea.ts | 43 ++++++--- .../controlLayers/konva/CanvasTool.ts | 7 +- .../controlLayers/store/bboxReducers.ts | 32 ------- .../controlLayers/store/canvasV2Slice.ts | 1 - .../controlLayers/store/layersReducers.ts | 37 +++---- 13 files changed, 227 insertions(+), 258 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index c551fa88b7..80c1f2bebd 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -5,9 +5,10 @@ import { parseify } from 'common/util/serialize'; import { caImageChanged, ipaImageChanged, - layerAddedFromImage, + layerAdded, rgIPAdapterImageChanged, } from 'features/controlLayers/store/canvasV2Slice'; +import type { CanvasLayerState } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { isValidDrop } from 'features/dnd/util/isValidDrop'; @@ -106,7 +107,13 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => activeData.payloadType === 'IMAGE_DTO' && activeData.payload.imageDTO ) { - dispatch(layerAddedFromImage({ imageObject: imageDTOToImageObject(activeData.payload.imageDTO) })); + const imageObject = imageDTOToImageObject(activeData.payload.imageDTO); + const { x, y } = getState().canvasV2.bbox.rect; + const overrides: Partial = { + objects: [imageObject], + position: { x, y }, + }; + dispatch(layerAdded({ overrides, isSelected: true })); return; } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index aee648be93..2c4ba2932b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -15,7 +15,7 @@ export const AddLayerButton = memo(() => { dispatch(rgAdded()); }, [dispatch]); const addRasterLayer = useCallback(() => { - dispatch(layerAdded()); + dispatch(layerAdded({ isSelected: true })); }, [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts index 7e72ef0ca7..18defa079f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts @@ -1,5 +1,6 @@ import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasPreview } from 'features/controlLayers/konva/CanvasPreview'; import type { Rect } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { atom } from 'nanostores'; @@ -23,6 +24,7 @@ export class CanvasBbox { static CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; static NO_ANCHORS: string[] = []; + parent: CanvasPreview; manager: CanvasManager; konva: { @@ -31,8 +33,9 @@ export class CanvasBbox { transformer: Konva.Transformer; }; - constructor(manager: CanvasManager) { - this.manager = manager; + constructor(parent: CanvasPreview) { + this.parent = parent; + this.manager = this.parent.manager; // Create a stash to hold onto the last aspect ratio of the bbox - this allows for locking the aspect ratio when // transforming the bbox. const bbox = this.manager.stateApi.getBbox(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index cd10e482b7..c5c0e810d7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -2,6 +2,7 @@ import { Mutex } from 'async-mutex'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; +import type { CanvasStagingArea } from 'features/controlLayers/konva/CanvasStagingArea'; import { FILTER_MAP } from 'features/controlLayers/konva/filters'; import { loadImage } from 'features/controlLayers/konva/util'; import type { CanvasImageState, GetLoggingContext } from 'features/controlLayers/store/types'; @@ -19,7 +20,7 @@ export class CanvasImageRenderer { static PLACEHOLDER_TEXT_NAME = `${CanvasImageRenderer.TYPE}_placeholder-text`; id: string; - parent: CanvasObjectRenderer; + parent: CanvasObjectRenderer | CanvasStagingArea; manager: CanvasManager; log: Logger; getLoggingContext: GetLoggingContext; @@ -36,7 +37,7 @@ export class CanvasImageRenderer { isError: boolean = false; mutex = new Mutex(); - constructor(state: CanvasImageState, parent: CanvasObjectRenderer) { + constructor(state: CanvasImageState, parent: CanvasObjectRenderer | CanvasStagingArea) { const { id, image } = state; const { width, height } = image; this.id = id; @@ -97,18 +98,16 @@ export class CanvasImageRenderer { this.onFailedToLoadImage(); return; } + // Load the thumbnail first, but let the image load in parallel loadImage(imageDTO.thumbnail_url) .then((thumbnailElement) => { this.thumbnailElement = thumbnailElement; - this.mutex.runExclusive(this.updateImageElement); - }) - .catch(this.onFailedToLoadImage); - loadImage(imageDTO.image_url) - .then((imageElement) => { - this.imageElement = imageElement; - this.mutex.runExclusive(this.updateImageElement); + this.updateImageElement(); }) .catch(this.onFailedToLoadImage); + + this.imageElement = await loadImage(imageDTO.image_url); + await this.updateImageElement(); } catch { this.onFailedToLoadImage(); } @@ -123,36 +122,50 @@ export class CanvasImageRenderer { this.konva.placeholder.group.visible(true); }; - updateImageElement = () => { - const element = this.imageElement ?? this.thumbnailElement; + updateImageElement = async () => { + const release = await this.mutex.acquire(); - if (element) { - if (this.konva.image && this.konva.image.image() !== element) { - this.konva.image.setAttrs({ - image: element, - }); - } else { - this.konva.image = new Konva.Image({ - name: CanvasImageRenderer.IMAGE_NAME, - listening: false, - image: element, - width: this.state.image.width, - height: this.state.image.height, - }); - this.konva.group.add(this.konva.image); + try { + const element = this.imageElement ?? this.thumbnailElement; + const { width, height } = this.state.image; + + if (element) { + if (this.konva.image) { + this.log.trace('Updating Konva image attrs'); + this.konva.image.setAttrs({ + image: element, + width, + height, + }); + } else { + this.log.trace('Creating new Konva image'); + this.konva.image = new Konva.Image({ + name: CanvasImageRenderer.IMAGE_NAME, + listening: false, + image: element, + width, + height, + }); + this.konva.group.add(this.konva.image); + } + + if (this.state.filters.length > 0) { + this.konva.image.cache(); + this.konva.image.filters(this.state.filters.map((f) => FILTER_MAP[f])); + } else { + this.konva.image.clearCache(); + this.konva.image.filters([]); + } + + this.konva.placeholder.rect.setAttrs({ width, height }); + this.konva.placeholder.text.setAttrs({ width, height, fontSize: width / 16 }); + + this.isLoading = false; + this.isError = false; + this.konva.placeholder.group.visible(false); } - - if (this.state.filters.length > 0) { - this.konva.image.cache(); - this.konva.image.filters(this.state.filters.map((f) => FILTER_MAP[f])); - } else { - this.konva.image.clearCache(); - this.konva.image.filters([]); - } - - this.isLoading = false; - this.isError = false; - this.konva.placeholder.group.visible(false); + } finally { + release(); } }; @@ -173,8 +186,6 @@ export class CanvasImageRenderer { this.konva.image?.clearCache(); this.konva.image?.filters([]); } - this.konva.placeholder.rect.setAttrs({ width, height }); - this.konva.placeholder.text.setAttrs({ width, height, fontSize: width / 16 }); this.state = state; return true; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 41d1423d86..90d73eaa35 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -7,7 +7,6 @@ import type { CanvasBrushLineRenderer } from 'features/controlLayers/konva/Canva import type { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; import type { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; -import { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview'; import type { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; import type { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import { MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/controlLayers/konva/constants'; @@ -19,7 +18,6 @@ import { nanoid, } from 'features/controlLayers/konva/util'; import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker'; -import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice'; import type { CanvasControlAdapterState, CanvasEntityIdentifier, @@ -49,14 +47,12 @@ import type { ImageCategory, ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; import { CanvasBackground } from './CanvasBackground'; -import { CanvasBbox } from './CanvasBbox'; import { CanvasControlAdapter } from './CanvasControlAdapter'; import { CanvasLayerAdapter } from './CanvasLayerAdapter'; import { CanvasMaskAdapter } from './CanvasMaskAdapter'; import { CanvasPreview } from './CanvasPreview'; import { CanvasStagingArea } from './CanvasStagingArea'; import { CanvasStateApi } from './CanvasStateApi'; -import { CanvasTool } from './CanvasTool'; import { setStageEventHandlers } from './events'; // type Extents = { @@ -159,15 +155,6 @@ export class CanvasManager { this.transformingEntity = new PubSub(null); this.toolState = new PubSub(this.stateApi.getToolState()); - this.currentFill = new PubSub(this.getCurrentFill()); - this.selectedEntityIdentifier = new PubSub( - this.stateApi.getState().selectedEntityIdentifier, - (a, b) => a?.id === b?.id - ); - this.selectedEntity = new PubSub( - this.getSelectedEntity(), - (a, b) => a?.state === b?.state && a?.adapter === b?.adapter - ); this._prevState = this.stateApi.getState(); @@ -187,13 +174,8 @@ export class CanvasManager { uploadImage, }; - this.preview = new CanvasPreview( - new CanvasBbox(this), - new CanvasTool(this), - new CanvasStagingArea(this), - new CanvasProgressPreview(this) - ); - this.stage.add(this.preview.layer); + this.preview = new CanvasPreview(this); + this.stage.add(this.preview.getLayer()); this.background = new CanvasBackground(this); this.stage.add(this.background.konva.layer); @@ -226,6 +208,16 @@ export class CanvasManager { this.log.error('Worker message error'); }; + this.currentFill = new PubSub(this.getCurrentFill()); + this.selectedEntityIdentifier = new PubSub( + this.stateApi.getState().selectedEntityIdentifier, + (a, b) => a?.id === b?.id + ); + this.selectedEntity = new PubSub( + this.getSelectedEntity(), + (a, b) => a?.state === b?.state && a?.adapter === b?.adapter + ); + this.inpaintMask = new CanvasMaskAdapter(this.stateApi.getInpaintMaskState(), this); this.stage.add(this.inpaintMask.konva.layer); } @@ -249,10 +241,6 @@ export class CanvasManager { this._worker.postMessage(task, [data.buffer]); } - async renderProgressPreview() { - await this.preview.progressPreview.render(this.stateApi.$lastProgressEvent.get()); - } - async renderControlAdapters() { const { entities } = this.stateApi.getControlAdaptersState(); @@ -291,7 +279,7 @@ export class CanvasManager { this.regions.get(rg.id)?.konva.layer.zIndex(++zIndex); } this.inpaintMask.konva.layer.zIndex(++zIndex); - this.preview.layer.zIndex(++zIndex); + this.preview.getLayer().zIndex(++zIndex); } fitStageToContainer() { @@ -611,25 +599,6 @@ export class CanvasManager { const unsubscribeRenderer = this._store.subscribe(this.render); - // When we this flag, we need to render the staging area - const unsubscribeShouldShowStagedImage = $shouldShowStagedImage.subscribe( - async (shouldShowStagedImage, prevShouldShowStagedImage) => { - if (shouldShowStagedImage !== prevShouldShowStagedImage) { - this.log.debug('Rendering staging area'); - await this.preview.stagingArea.render(); - } - } - ); - - const unsubscribeLastProgressEvent = $lastProgressEvent.subscribe( - async (lastProgressEvent, prevLastProgressEvent) => { - if (lastProgressEvent !== prevLastProgressEvent) { - this.log.debug('Rendering progress image'); - await this.preview.progressPreview.render(lastProgressEvent); - } - } - ); - this.log.debug('First render of konva stage'); this.preview.tool.render(); this.render(); @@ -650,8 +619,6 @@ export class CanvasManager { this.preview.destroy(); unsubscribeRenderer(); unsubscribeListeners(); - unsubscribeShouldShowStagedImage(); - unsubscribeLastProgressEvent(); resizeObserver.disconnect(); }; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts index ef0f6a579b..b5969299cc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts @@ -1,40 +1,51 @@ -import type { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasProgressImage } from 'features/controlLayers/konva/CanvasProgressImage'; import Konva from 'konva'; -import type { CanvasBbox } from './CanvasBbox'; -import type { CanvasStagingArea } from './CanvasStagingArea'; -import type { CanvasTool } from './CanvasTool'; +import { CanvasBbox } from './CanvasBbox'; +import { CanvasStagingArea } from './CanvasStagingArea'; +import { CanvasTool } from './CanvasTool'; export class CanvasPreview { - layer: Konva.Layer; + manager: CanvasManager; + + konva: { + layer: Konva.Layer; + }; + tool: CanvasTool; bbox: CanvasBbox; stagingArea: CanvasStagingArea; - progressPreview: CanvasProgressPreview; + progressImage: CanvasProgressImage; - constructor( - bbox: CanvasBbox, - tool: CanvasTool, - stagingArea: CanvasStagingArea, - progressPreview: CanvasProgressPreview - ) { - this.layer = new Konva.Layer({ listening: true, imageSmoothingEnabled: false }); + constructor(manager: CanvasManager) { + this.manager = manager; + this.konva = { + layer: new Konva.Layer({ listening: true, imageSmoothingEnabled: false }), + }; - this.stagingArea = stagingArea; - this.layer.add(this.stagingArea.konva.group); + this.stagingArea = new CanvasStagingArea(this); + this.konva.layer.add(...this.stagingArea.getNodes()); - this.bbox = bbox; - this.layer.add(this.bbox.konva.group); + this.progressImage = new CanvasProgressImage(this); + this.konva.layer.add(...this.progressImage.getNodes()); - this.tool = tool; - this.layer.add(this.tool.konva.group); + this.bbox = new CanvasBbox(this); + this.konva.layer.add(this.bbox.konva.group); - this.progressPreview = progressPreview; - this.layer.add(this.progressPreview.konva.group); + this.tool = new CanvasTool(this); + this.konva.layer.add(this.tool.konva.group); } + getLayer = () => { + return this.konva.layer; + }; + destroy() { + // this.stagingArea.destroy(); // TODO(psyche): implement destroy + this.progressImage.destroy(); + // this.bbox.destroy(); // TODO(psyche): implement destroy this.tool.destroy(); - this.layer.destroy(); + this.konva.layer.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts index edda6c26f5..c9ffe895d4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts @@ -1,5 +1,9 @@ -import { loadImage } from 'features/controlLayers/konva/util'; +import { Mutex } from 'async-mutex'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasPreview } from 'features/controlLayers/konva/CanvasPreview'; +import { getPrefixedId, loadImage } from 'features/controlLayers/konva/util'; import Konva from 'konva'; +import type { InvocationDenoiseProgressEvent } from 'services/events/types'; export class CanvasProgressImage { static NAME_PREFIX = 'progress-image'; @@ -7,53 +11,86 @@ export class CanvasProgressImage { static IMAGE_NAME = `${CanvasProgressImage.NAME_PREFIX}_image`; id: string; - progressImageId: string | null; + parent: CanvasPreview; + manager: CanvasManager; + + /** + * A set of subscriptions that should be cleaned up when the transformer is destroyed. + */ + subscriptions: Set<() => void> = new Set(); + + progressImageId: string | null = null; konva: { group: Konva.Group; image: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately }; - isLoading: boolean; - isError: boolean; + isLoading: boolean = false; + isError: boolean = false; + imageElement: HTMLImageElement | null = null; - constructor(arg: { id: string }) { - const { id } = arg; + lastProgressEvent: InvocationDenoiseProgressEvent | null = null; + + mutex: Mutex = new Mutex(); + + constructor(parent: CanvasPreview) { + this.id = getPrefixedId(CanvasProgressImage.NAME_PREFIX); + this.parent = parent; + this.manager = parent.manager; this.konva = { group: new Konva.Group({ name: CanvasProgressImage.GROUP_NAME, listening: false }), image: null, }; - this.id = id; - this.progressImageId = null; - this.isLoading = false; - this.isError = false; + + this.manager.stateApi.$lastProgressEvent.listen((event) => { + this.lastProgressEvent = event; + this.render(); + }); } - async updateImageSource( - progressImageId: string, - dataURL: string, - x: number, - y: number, - width: number, - height: number - ) { - if (this.isLoading) { + getNodes = () => { + return [this.konva.group]; + }; + + render = async () => { + const release = await this.mutex.acquire(); + + if (!this.lastProgressEvent) { + this.konva.group.visible(false); + this.imageElement = null; + this.isLoading = false; + this.isError = false; + release(); return; } + + const { isStaging } = this.manager.stateApi.getSession(); + + if (!isStaging) { + release(); + return; + } + this.isLoading = true; + + const { x, y } = this.manager.stateApi.getBbox().rect; + const { dataURL, width, height } = this.lastProgressEvent.progress_image; try { - const imageEl = await loadImage(dataURL); + this.imageElement = await loadImage(dataURL); if (this.konva.image) { + console.log('UPDATING PROGRESS IMAGE') this.konva.image.setAttrs({ - image: imageEl, + image: this.imageElement, x, y, width, height, }); } else { + console.log('CREATING NEW PROGRESS IMAGE') this.konva.image = new Konva.Image({ name: CanvasProgressImage.IMAGE_NAME, listening: false, - image: imageEl, + image: this.imageElement, x, y, width, @@ -61,14 +98,19 @@ export class CanvasProgressImage { }); this.konva.group.add(this.konva.image); } - this.isLoading = false; - this.id = progressImageId; + this.konva.group.visible(true); } catch { this.isError = true; + } finally { + this.isLoading = false; + release(); } - } + }; - destroy() { + destroy = () => { + for (const unsubscribe of this.subscriptions) { + unsubscribe(); + } this.konva.group.destroy(); - } + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts deleted file mode 100644 index 95c7910b52..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasProgressImage } from 'features/controlLayers/konva/CanvasProgressImage'; -import Konva from 'konva'; -import type { InvocationDenoiseProgressEvent } from 'services/events/types'; - -export class CanvasProgressPreview { - static NAME_PREFIX = 'progress-preview'; - static GROUP_NAME = `${CanvasProgressPreview.NAME_PREFIX}_group`; - - konva: { - group: Konva.Group; - progressImage: CanvasProgressImage; - }; - manager: CanvasManager; - - constructor(manager: CanvasManager) { - this.manager = manager; - this.konva = { - group: new Konva.Group({ name: CanvasProgressPreview.GROUP_NAME, listening: false }), - progressImage: new CanvasProgressImage({ id: 'progress-image' }), - }; - this.konva.group.add(this.konva.progressImage.konva.group); - } - - async render(lastProgressEvent: InvocationDenoiseProgressEvent | null) { - const bboxRect = this.manager.stateApi.getBbox().rect; - const session = this.manager.stateApi.getSession(); - - if (lastProgressEvent && session.isStaging) { - const { invocation, step, progress_image } = lastProgressEvent; - const { dataURL } = progress_image; - const { x, y, width, height } = bboxRect; - const progressImageId = `${invocation.id}_${step}`; - if ( - !this.konva.progressImage.isLoading && - !this.konva.progressImage.isError && - this.konva.progressImage.progressImageId !== progressImageId - ) { - await this.konva.progressImage.updateImageSource(progressImageId, dataURL, x, y, width, height); - this.konva.progressImage.konva.group.visible(true); - } - } else { - this.konva.progressImage.konva.group.visible(false); - } - } -} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index 50872886ef..be17df0404 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -1,5 +1,6 @@ import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasPreview } from 'features/controlLayers/konva/CanvasPreview'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { GetLoggingContext, StagingAreaImage } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -10,6 +11,7 @@ export class CanvasStagingArea { static GROUP_NAME = `${CanvasStagingArea.TYPE}_group`; id: string; + parent: CanvasPreview; manager: CanvasManager; log: Logger; getLoggingContext: GetLoggingContext; @@ -19,9 +21,15 @@ export class CanvasStagingArea { image: CanvasImageRenderer | null; selectedImage: StagingAreaImage | null; - constructor(manager: CanvasManager) { + /** + * A set of subscriptions that should be cleaned up when the transformer is destroyed. + */ + subscriptions: Set<() => void> = new Set(); + + constructor(parent: CanvasPreview) { this.id = getPrefixedId(CanvasStagingArea.TYPE); - this.manager = manager; + this.parent = parent; + this.manager = this.parent.manager; this.getLoggingContext = this.manager.buildGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); this.log.debug('Creating staging area'); @@ -29,14 +37,17 @@ export class CanvasStagingArea { this.konva = { group: new Konva.Group({ name: CanvasStagingArea.GROUP_NAME, listening: false }) }; this.image = null; this.selectedImage = null; + + this.subscriptions.add(this.manager.stateApi.$shouldShowStagedImage.listen(this.render)); } render = async () => { const session = this.manager.stateApi.getSession(); - const bboxRect = this.manager.stateApi.getBbox().rect; + const { rect } = this.manager.stateApi.getBbox(); const shouldShowStagedImage = this.manager.stateApi.$shouldShowStagedImage.get(); this.selectedImage = session.stagedImages[session.selectedStagedImageIndex] ?? null; + this.konva.group.position({ x: rect.x, y: rect.y }); if (this.selectedImage) { const { imageDTO, offsetX, offsetY } = this.selectedImage; @@ -47,10 +58,6 @@ export class CanvasStagingArea { { id: 'staging-area-image', type: 'image', - x: 0, - y: 0, - width, - height, filters: [], image: { image_name: image_name, @@ -63,11 +70,7 @@ export class CanvasStagingArea { this.konva.group.add(this.image.konva.group); } - if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) { - this.image.konva.image?.width(imageDTO.width); - this.image.konva.image?.height(imageDTO.height); - this.image.konva.group.x(bboxRect.x + offsetX); - this.image.konva.group.y(bboxRect.y + offsetY); + if (!this.image.isLoading && !this.image.isError) { await this.image.updateImageSource(imageDTO.image_name); this.manager.stateApi.$lastProgressEvent.set(null); } @@ -77,6 +80,22 @@ export class CanvasStagingArea { } }; + getNodes = () => { + return [this.konva.group]; + }; + + destroy = () => { + if (this.image) { + this.image.destroy(); + } + for (const unsubscribe of this.subscriptions) { + unsubscribe(); + } + for (const node of this.getNodes()) { + node.destroy(); + } + }; + repr = () => { return { id: this.id, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index 11aeabac4d..1d2b780906 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -1,5 +1,6 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasPreview } from 'features/controlLayers/konva/CanvasPreview'; import { BRUSH_BORDER_INNER_COLOR, BRUSH_BORDER_OUTER_COLOR, @@ -25,6 +26,7 @@ export class CanvasTool { static ERASER_INNER_BORDER_CIRCLE_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_inner-border-circle`; static ERASER_OUTER_BORDER_CIRCLE_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_outer-border-circle`; + parent: CanvasPreview; manager: CanvasManager; konva: { group: Konva.Group; @@ -47,8 +49,9 @@ export class CanvasTool { */ subscriptions: Set<() => void> = new Set(); - constructor(manager: CanvasManager) { - this.manager = manager; + constructor(parent: CanvasPreview) { + this.parent = parent; + this.manager = this.parent.manager; this.konva = { group: new Konva.Group({ name: CanvasTool.GROUP_NAME }), brush: { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts index a1c276f4ae..81f9d2fd90 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts @@ -48,13 +48,6 @@ export const bboxReducers = { state.bbox.aspectRatio.id = 'Free'; state.bbox.aspectRatio.isLocked = false; } - - if (!state.session.isActive) { - if (state.initialImage.imageObject) { - state.initialImage.imageObject.width = state.bbox.rect.width; - state.initialImage.imageObject.height = state.bbox.rect.height; - } - } }, bboxHeightChanged: ( state, @@ -73,13 +66,6 @@ export const bboxReducers = { state.bbox.aspectRatio.id = 'Free'; state.bbox.aspectRatio.isLocked = false; } - - if (!state.session.isActive) { - if (state.initialImage.imageObject) { - state.initialImage.imageObject.width = state.bbox.rect.width; - state.initialImage.imageObject.height = state.bbox.rect.height; - } - } }, bboxAspectRatioLockToggled: (state) => { state.bbox.aspectRatio.isLocked = !state.bbox.aspectRatio.isLocked; @@ -99,12 +85,6 @@ export const bboxReducers = { state.bbox.rect.width = width; state.bbox.rect.height = height; } - if (!state.session.isActive) { - if (state.initialImage.imageObject) { - state.initialImage.imageObject.width = state.bbox.rect.width; - state.initialImage.imageObject.height = state.bbox.rect.height; - } - } }, bboxDimensionsSwapped: (state) => { state.bbox.aspectRatio.value = 1 / state.bbox.aspectRatio.value; @@ -122,12 +102,6 @@ export const bboxReducers = { state.bbox.rect.height = height; state.bbox.aspectRatio.id = ASPECT_RATIO_MAP[state.bbox.aspectRatio.id].inverseID; } - if (!state.session.isActive) { - if (state.initialImage.imageObject) { - state.initialImage.imageObject.width = state.bbox.rect.width; - state.initialImage.imageObject.height = state.bbox.rect.height; - } - } }, bboxSizeOptimized: (state) => { const optimalDimension = getOptimalDimension(state.params.model); @@ -140,11 +114,5 @@ export const bboxReducers = { state.bbox.rect.width = optimalDimension; state.bbox.rect.height = optimalDimension; } - if (!state.session.isActive) { - if (state.initialImage.imageObject) { - state.initialImage.imageObject.width = state.bbox.rect.width; - state.initialImage.imageObject.height = state.bbox.rect.height; - } - } }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index ca37dc9517..7d5ac7a91a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -411,7 +411,6 @@ export const { bboxSizeOptimized, // layers layerAdded, - layerAddedFromImage, layerRecalled, layerOpacityChanged, layerAllDeleted, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 48e01cd1f3..133e121339 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -4,7 +4,7 @@ import { merge } from 'lodash-es'; import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; -import type { CanvasImageState, CanvasLayerState, CanvasV2State } from './types'; +import type { CanvasLayerState, CanvasV2State } from './types'; import { imageDTOToImageWithDims } from './types'; export const selectLayer = (state: CanvasV2State, id: string) => state.layers.entities.find((layer) => layer.id === id); @@ -16,8 +16,11 @@ export const selectLayerOrThrow = (state: CanvasV2State, id: string) => { export const layersReducers = { layerAdded: { - reducer: (state, action: PayloadAction<{ id: string; overrides?: Partial }>) => { - const { id } = action.payload; + reducer: ( + state, + action: PayloadAction<{ id: string; overrides?: Partial; isSelected?: boolean }> + ) => { + const { id, overrides, isSelected } = action.payload; const layer: CanvasLayerState = { id, type: 'layer', @@ -27,12 +30,14 @@ export const layersReducers = { position: { x: 0, y: 0 }, imageCache: null, }; - merge(layer, action.payload.overrides); + merge(layer, overrides); state.layers.entities.push(layer); - state.selectedEntityIdentifier = { type: 'layer', id }; + if (isSelected) { + state.selectedEntityIdentifier = { type: 'layer', id }; + } state.layers.imageCache = null; }, - prepare: (payload: { overrides?: Partial }) => ({ + prepare: (payload: { overrides?: Partial; isSelected?: boolean }) => ({ payload: { ...payload, id: getPrefixedId('layer') }, }), }, @@ -42,26 +47,6 @@ export const layersReducers = { state.selectedEntityIdentifier = { type: 'layer', id: data.id }; state.layers.imageCache = null; }, - layerAddedFromImage: { - reducer: (state, action: PayloadAction<{ id: string; imageObject: CanvasImageState }>) => { - const { id, imageObject } = action.payload; - const layer: CanvasLayerState = { - id, - type: 'layer', - isEnabled: true, - objects: [imageObject], - opacity: 1, - position: { x: 0, y: 0 }, - imageCache: null, - }; - state.layers.entities.push(layer); - state.selectedEntityIdentifier = { type: 'layer', id }; - state.layers.imageCache = null; - }, - prepare: (payload: { imageObject: CanvasImageState }) => ({ - payload: { ...payload, id: getPrefixedId('layer') }, - }), - }, layerAllDeleted: (state) => { state.layers.entities = []; state.layers.imageCache = null;