diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index 85704cd80d..d842efff11 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -1,19 +1,19 @@ import type { JSONObject } from 'common/types'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; -import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import type { CanvasBrushLineState } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasBrushLine { +export class CanvasBrushLineRenderer { static TYPE = 'brush_line'; - static GROUP_NAME = `${CanvasBrushLine.TYPE}_group`; - static LINE_NAME = `${CanvasBrushLine.TYPE}_line`; + static GROUP_NAME = `${CanvasBrushLineRenderer.TYPE}_group`; + static LINE_NAME = `${CanvasBrushLineRenderer.TYPE}_line`; id: string; - parent: CanvasLayer; + parent: CanvasObjectRenderer; manager: CanvasManager; log: Logger; getLoggingContext: (extra?: JSONObject) => JSONObject; @@ -23,8 +23,9 @@ export class CanvasBrushLine { group: Konva.Group; line: Konva.Line; }; + isFirstRender: boolean = false; - constructor(state: CanvasBrushLineState, parent: CanvasLayer) { + constructor(state: CanvasBrushLineState, parent: CanvasObjectRenderer) { const { id, strokeWidth, clip, color, points } = state; this.id = id; this.parent = parent; @@ -37,12 +38,12 @@ export class CanvasBrushLine { this.konva = { group: new Konva.Group({ - name: CanvasBrushLine.GROUP_NAME, + name: CanvasBrushLineRenderer.GROUP_NAME, clip, listening: false, }), line: new Konva.Line({ - name: CanvasBrushLine.LINE_NAME, + name: CanvasBrushLineRenderer.LINE_NAME, listening: false, shadowForStrokeEnabled: false, strokeWidth, @@ -59,8 +60,10 @@ export class CanvasBrushLine { this.state = state; } - update(state: CanvasBrushLineState, force?: boolean): boolean { + update(state: CanvasBrushLineState, force = this.isFirstRender): boolean { if (force || this.state !== state) { + this.isFirstRender = false; + this.log.trace({ state }, 'Updating brush line'); const { points, color, clip, strokeWidth } = state; this.konva.line.setAttrs({ @@ -72,9 +75,9 @@ export class CanvasBrushLine { }); this.state = state; return true; - } else { - return false; } + + return false; } destroy() { @@ -90,8 +93,9 @@ export class CanvasBrushLine { repr() { return { id: this.id, - type: CanvasBrushLine.TYPE, + type: CanvasBrushLineRenderer.TYPE, parent: this.parent.id, + isFirstRender: this.isFirstRender, state: deepClone(this.state), }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts index 64f65cfb8f..679dad49df 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts @@ -1,5 +1,5 @@ import { CanvasEntity } from 'features/controlLayers/konva/CanvasEntity'; -import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; +import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import { type CanvasControlAdapterState, isDrawingTool } from 'features/controlLayers/store/types'; @@ -21,7 +21,7 @@ export class CanvasControlAdapter extends CanvasEntity { objectGroup: Konva.Group; }; - image: CanvasImage | null; + image: CanvasImageRenderer | null; transformer: CanvasTransformer; constructor(state: CanvasControlAdapterState, manager: CanvasManager) { @@ -68,7 +68,7 @@ export class CanvasControlAdapter extends CanvasEntity { didDraw = true; } } else if (!this.image) { - this.image = new CanvasImage(imageObject, this); + this.image = new CanvasImageRenderer(imageObject, this); this.updateGroup(true); this.konva.objectGroup.add(this.image.konva.group); await this.image.updateImageSource(imageObject.image.name); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index fdff175b74..1f4679807d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -1,30 +1,31 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; -import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import type { CanvasEraserLineState, GetLoggingContext } from 'features/controlLayers/store/types'; import { RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasEraserLine { +export class CanvasEraserLineRenderer { static TYPE = 'eraser_line'; - static GROUP_NAME = `${CanvasEraserLine.TYPE}_group`; - static LINE_NAME = `${CanvasEraserLine.TYPE}_line`; + static GROUP_NAME = `${CanvasEraserLineRenderer.TYPE}_group`; + static LINE_NAME = `${CanvasEraserLineRenderer.TYPE}_line`; id: string; - parent: CanvasLayer; + parent: CanvasObjectRenderer; manager: CanvasManager; log: Logger; getLoggingContext: GetLoggingContext; + isFirstRender: boolean = false; state: CanvasEraserLineState; konva: { group: Konva.Group; line: Konva.Line; }; - constructor(state: CanvasEraserLineState, parent: CanvasLayer) { + constructor(state: CanvasEraserLineState, parent: CanvasObjectRenderer) { const { id, strokeWidth, clip, points } = state; this.id = id; this.parent = parent; @@ -36,12 +37,12 @@ export class CanvasEraserLine { this.konva = { group: new Konva.Group({ - name: CanvasEraserLine.GROUP_NAME, + name: CanvasEraserLineRenderer.GROUP_NAME, clip, listening: false, }), line: new Konva.Line({ - name: CanvasEraserLine.LINE_NAME, + name: CanvasEraserLineRenderer.LINE_NAME, listening: false, shadowForStrokeEnabled: false, strokeWidth, @@ -58,8 +59,10 @@ export class CanvasEraserLine { this.state = state; } - update(state: CanvasEraserLineState, force?: boolean): boolean { + update(state: CanvasEraserLineState, force = this.isFirstRender): boolean { if (force || this.state !== state) { + this.isFirstRender = false; + this.log.trace({ state }, 'Updating eraser line'); const { points, clip, strokeWidth } = state; this.konva.line.setAttrs({ @@ -70,9 +73,9 @@ export class CanvasEraserLine { }); this.state = state; return true; - } else { - return false; } + + return false; } destroy() { @@ -88,8 +91,9 @@ export class CanvasEraserLine { repr() { return { id: this.id, - type: CanvasEraserLine.TYPE, + type: CanvasEraserLineRenderer.TYPE, parent: this.parent.id, + isFirstRender: this.isFirstRender, state: deepClone(this.state), }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index 500a9fa208..77ec5a2a68 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -1,25 +1,24 @@ import { deepClone } from 'common/util/deepClone'; -import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { CanvasStagingArea } from 'features/controlLayers/konva/CanvasStagingArea'; +import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { FILTER_MAP } from 'features/controlLayers/konva/filters'; import { loadImage } from 'features/controlLayers/konva/util'; -import type { GetLoggingContext, CanvasImageState } from 'features/controlLayers/store/types'; +import type { CanvasImageState, GetLoggingContext } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; import type { Logger } from 'roarr'; import { getImageDTO } from 'services/api/endpoints/images'; -export class CanvasImage { +export class CanvasImageRenderer { static TYPE = 'image'; - static GROUP_NAME = `${CanvasImage.TYPE}_group`; - static IMAGE_NAME = `${CanvasImage.TYPE}_image`; - static PLACEHOLDER_GROUP_NAME = `${CanvasImage.TYPE}_placeholder-group`; - static PLACEHOLDER_RECT_NAME = `${CanvasImage.TYPE}_placeholder-rect`; - static PLACEHOLDER_TEXT_NAME = `${CanvasImage.TYPE}_placeholder-text`; + static GROUP_NAME = `${CanvasImageRenderer.TYPE}_group`; + static IMAGE_NAME = `${CanvasImageRenderer.TYPE}_image`; + static PLACEHOLDER_GROUP_NAME = `${CanvasImageRenderer.TYPE}_placeholder-group`; + static PLACEHOLDER_RECT_NAME = `${CanvasImageRenderer.TYPE}_placeholder-rect`; + static PLACEHOLDER_TEXT_NAME = `${CanvasImageRenderer.TYPE}_placeholder-text`; id: string; - parent: CanvasLayer | CanvasStagingArea; + parent: CanvasObjectRenderer; manager: CanvasManager; log: Logger; getLoggingContext: GetLoggingContext; @@ -33,8 +32,9 @@ export class CanvasImage { imageName: string | null; isLoading: boolean; isError: boolean; + isFirstRender: boolean = true; - constructor(state: CanvasImageState, parent: CanvasLayer | CanvasStagingArea) { + constructor(state: CanvasImageState, parent: CanvasObjectRenderer) { const { id, width, height, x, y } = state; this.id = id; this.parent = parent; @@ -45,18 +45,18 @@ export class CanvasImage { this.log.trace({ state }, 'Creating image'); this.konva = { - group: new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }), + group: new Konva.Group({ name: CanvasImageRenderer.GROUP_NAME, listening: false, x, y }), placeholder: { - group: new Konva.Group({ name: CanvasImage.PLACEHOLDER_GROUP_NAME, listening: false }), + group: new Konva.Group({ name: CanvasImageRenderer.PLACEHOLDER_GROUP_NAME, listening: false }), rect: new Konva.Rect({ - name: CanvasImage.PLACEHOLDER_RECT_NAME, + name: CanvasImageRenderer.PLACEHOLDER_RECT_NAME, fill: 'hsl(220 12% 45% / 1)', // 'base.500' width, height, listening: false, }), text: new Konva.Text({ - name: CanvasImage.PLACEHOLDER_TEXT_NAME, + name: CanvasImageRenderer.PLACEHOLDER_TEXT_NAME, fill: 'hsl(220 12% 10% / 1)', // 'base.900' width, height, @@ -81,7 +81,7 @@ export class CanvasImage { this.state = state; } - async updateImageSource(imageName: string) { + updateImageSource = async (imageName: string) => { try { this.log.trace({ imageName }, 'Updating image source'); @@ -106,7 +106,7 @@ export class CanvasImage { }); } else { this.konva.image = new Konva.Image({ - name: CanvasImage.IMAGE_NAME, + name: CanvasImageRenderer.IMAGE_NAME, listening: false, image: imageEl, width: this.state.width, @@ -136,14 +136,16 @@ export class CanvasImage { this.konva.placeholder.text.text(t('common.imageFailedToLoad', 'Image Failed to Load')); this.konva.placeholder.group.visible(true); } - } + }; + + update = async (state: CanvasImageState, force = this.isFirstRender): Promise => { + if (force || this.state !== state) { + this.isFirstRender = false; - async update(state: CanvasImageState, force?: boolean): Promise { - if (this.state !== state || force) { this.log.trace({ state }, 'Updating image'); const { width, height, x, y, image, filters } = state; - if (this.state.image.name !== image.name || force) { + if (force || (this.state.image.name !== image.name && !this.isLoading)) { await this.updateImageSource(image.name); } this.konva.image?.setAttrs({ x, y, width, height }); @@ -158,30 +160,31 @@ export class CanvasImage { this.konva.placeholder.text.setAttrs({ width, height, fontSize: width / 16 }); this.state = state; return true; - } else { - return false; } - } - destroy() { + return false; + }; + + destroy = () => { this.log.trace('Destroying image'); this.konva.group.destroy(); - } + }; - setVisibility(isVisible: boolean): void { + setVisibility = (isVisible: boolean): void => { this.log.trace({ isVisible }, 'Setting image visibility'); this.konva.group.visible(isVisible); - } + }; - repr() { + repr = () => { return { id: this.id, - type: CanvasImage.TYPE, + type: CanvasImageRenderer.TYPE, parent: this.parent.id, imageName: this.imageName, isLoading: this.isLoading, isError: this.isError, + isFirstRender: this.isFirstRender, state: deepClone(this.state), }; - } + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts index 37fe66c96b..52bb4a398a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts @@ -1,4 +1,4 @@ -import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; +import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { InitialImageEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -20,7 +20,7 @@ export class CanvasInitialImage { objectGroup: Konva.Group; }; - image: CanvasImage | null; + image: CanvasImageRenderer | null; constructor(state: InitialImageEntity, manager: CanvasManager) { this.manager = manager; @@ -45,7 +45,7 @@ export class CanvasInitialImage { } if (!this.image) { - this.image = new CanvasImage(this.state.imageObject); + this.image = new CanvasImageRenderer(this.state.imageObject); this.konva.objectGroup.add(this.image.konva.group); await this.image.update(this.state.imageObject, true); } else if (!this.image.isLoading && !this.image.isError) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index 965c7fc5df..681ded2bb9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -1,8 +1,8 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine'; -import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'; +import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine'; +import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; +import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; import { mapId } from 'features/controlLayers/konva/util'; import type { CanvasBrushLineState, CanvasEraserLineState, CanvasInpaintMaskState, CanvasRectState } from 'features/controlLayers/store/types'; @@ -31,7 +31,7 @@ export class CanvasInpaintMask { transformer: Konva.Transformer; compositingRect: Konva.Rect; }; - objects: Map; + objects: Map; constructor(state: CanvasInpaintMaskState, manager: CanvasManager) { this.manager = manager; @@ -156,10 +156,10 @@ export class CanvasInpaintMask { private async renderObject(obj: CanvasInpaintMaskState['objects'][number], force = false): Promise { if (obj.type === 'brush_line') { let brushLine = this.objects.get(obj.id); - assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); + assert(brushLine instanceof CanvasBrushLineRenderer || brushLine === undefined); if (!brushLine) { - brushLine = new CanvasBrushLine(obj); + brushLine = new CanvasBrushLineRenderer(obj); this.objects.set(brushLine.id, brushLine); this.konva.objectGroup.add(brushLine.konva.group); return true; @@ -170,10 +170,10 @@ export class CanvasInpaintMask { } } else if (obj.type === 'eraser_line') { let eraserLine = this.objects.get(obj.id); - assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined); + assert(eraserLine instanceof CanvasEraserLineRenderer || eraserLine === undefined); if (!eraserLine) { - eraserLine = new CanvasEraserLine(obj); + eraserLine = new CanvasEraserLineRenderer(obj); this.objects.set(eraserLine.id, eraserLine); this.konva.objectGroup.add(eraserLine.konva.group); return true; @@ -184,10 +184,10 @@ export class CanvasInpaintMask { } } else if (obj.type === 'rect') { let rect = this.objects.get(obj.id); - assert(rect instanceof CanvasRect || rect === undefined); + assert(rect instanceof CanvasRectRenderer || rect === undefined); if (!rect) { - rect = new CanvasRect(obj); + rect = new CanvasRectRenderer(obj); this.objects.set(rect.id, rect); this.konva.objectGroup.add(rect.konva.group); return true; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index e927909530..684f7b2d6e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -1,18 +1,12 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; -import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine'; -import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'; -import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; +import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; -import { getPrefixedId, konvaNodeToBlob, mapId, previewBlob } from 'features/controlLayers/konva/util'; +import { konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util'; import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice'; import type { - CanvasBrushLineState, - CanvasEraserLineState, CanvasLayerState, - CanvasRectState, CanvasV2State, Coordinate, GetLoggingContext, @@ -23,34 +17,28 @@ import Konva from 'konva'; import { debounce, get } from 'lodash-es'; import type { Logger } from 'roarr'; import { uploadImage } from 'services/api/endpoints/images'; -import { assert } from 'tsafe'; export class CanvasLayer { static TYPE = 'layer'; - static LAYER_NAME = `${CanvasLayer.TYPE}_layer`; - static TRANSFORMER_NAME = `${CanvasLayer.TYPE}_transformer`; - static INTERACTION_RECT_NAME = `${CanvasLayer.TYPE}_interaction-rect`; - static GROUP_NAME = `${CanvasLayer.TYPE}_group`; - static OBJECT_GROUP_NAME = `${CanvasLayer.TYPE}_object-group`; - static BBOX_NAME = `${CanvasLayer.TYPE}_bbox`; + static KONVA_LAYER_NAME = `${CanvasLayer.TYPE}_layer`; + static KONVA_OBJECT_GROUP_NAME = `${CanvasLayer.TYPE}_object-group`; id: string; manager: CanvasManager; log: Logger; getLoggingContext: GetLoggingContext; - drawingBuffer: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null; state: CanvasLayerState; konva: { layer: Konva.Layer; objectGroup: Konva.Group; }; - objects: Map; transformer: CanvasTransformer; + renderer: CanvasObjectRenderer; + isFirstRender: boolean = true; bboxNeedsUpdate: boolean; - isFirstRender: boolean; isTransforming: boolean; isPendingBboxCalculation: boolean; @@ -67,26 +55,24 @@ export class CanvasLayer { this.konva = { layer: new Konva.Layer({ id: this.id, - name: CanvasLayer.LAYER_NAME, + name: CanvasLayer.KONVA_LAYER_NAME, listening: false, imageSmoothingEnabled: false, }), - objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }), + objectGroup: new Konva.Group({ name: CanvasLayer.KONVA_OBJECT_GROUP_NAME, listening: false }), }; - this.transformer = new CanvasTransformer(this, this.konva.objectGroup); + this.transformer = new CanvasTransformer(this); + this.renderer = new CanvasObjectRenderer(this); this.konva.layer.add(this.konva.objectGroup); this.konva.layer.add(...this.transformer.getNodes()); - this.objects = new Map(); - this.drawingBuffer = null; this.state = state; this.rect = this.getDefaultRect(); this.bbox = this.getDefaultRect(); this.bboxNeedsUpdate = true; this.isTransforming = false; - this.isFirstRender = true; this.isPendingBboxCalculation = false; } @@ -94,47 +80,10 @@ export class CanvasLayer { this.log.debug('Destroying layer'); // We need to call the destroy method on all children so they can do their own cleanup. this.transformer.destroy(); - for (const obj of this.objects.values()) { - obj.destroy(); - } + this.renderer.destroy(); this.konva.layer.destroy(); }; - getDrawingBuffer = () => { - return this.drawingBuffer; - }; - - setDrawingBuffer = async (obj: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null) => { - if (obj) { - this.drawingBuffer = obj; - await this._renderObject(this.drawingBuffer, true); - } else { - this.drawingBuffer = null; - } - }; - - finalizeDrawingBuffer = async () => { - if (!this.drawingBuffer) { - return; - } - const drawingBuffer = this.drawingBuffer; - await this.setDrawingBuffer(null); - - // We need to give the objects a fresh ID else they will be considered the same object when they are re-rendered as - // a non-buffer object, and we won't trigger things like bbox calculation - - if (drawingBuffer.type === 'brush_line') { - drawingBuffer.id = getPrefixedId('brush_line'); - this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: drawingBuffer }, 'layer'); - } else if (drawingBuffer.type === 'eraser_line') { - drawingBuffer.id = getPrefixedId('brush_line'); - this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: drawingBuffer }, 'layer'); - } else if (drawingBuffer.type === 'rect') { - drawingBuffer.id = getPrefixedId('brush_line'); - this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: drawingBuffer }, 'layer'); - } - }; - update = async (arg?: { state: CanvasLayerState; toolState: CanvasV2State['tool']; isSelected: boolean }) => { const state = get(arg, 'state', this.state); const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState()); @@ -173,8 +122,7 @@ export class CanvasLayer { updateVisibility = (arg?: { isEnabled: boolean }) => { this.log.trace('Updating visibility'); const isEnabled = get(arg, 'isEnabled', this.state.isEnabled); - const hasObjects = this.objects.size > 0 || this.drawingBuffer !== null; - this.konva.layer.visible(isEnabled && hasObjects); + this.konva.layer.visible(isEnabled && this.renderer.hasObjects()); }; updatePosition = (arg?: { position: Coordinate }) => { @@ -196,30 +144,7 @@ export class CanvasLayer { const objects = get(arg, 'objects', this.state.objects); - const objectIds = objects.map(mapId); - - let didUpdate = false; - - // Destroy any objects that are no longer in state - for (const object of this.objects.values()) { - if (!objectIds.includes(object.id) && object.id !== this.drawingBuffer?.id) { - this.objects.delete(object.id); - object.destroy(); - didUpdate = true; - } - } - - for (const obj of objects) { - if (await this._renderObject(obj)) { - didUpdate = true; - } - } - - if (this.drawingBuffer) { - if (await this._renderObject(this.drawingBuffer)) { - didUpdate = true; - } - } + const didUpdate = await this.renderer.render(objects); if (didUpdate) { this.calculateBbox(); @@ -240,7 +165,7 @@ export class CanvasLayer { const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState()); const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id)); - if (this.objects.size === 0) { + if (!this.renderer.hasObjects()) { // The layer is totally empty, we can just disable the layer this.konva.layer.listening(false); this.transformer.setMode('off'); @@ -279,7 +204,7 @@ export class CanvasLayer { // eraser lines, fully clipped brush lines or if it has been fully erased. if (this.bbox.width === 0 || this.bbox.height === 0) { // We shouldn't reset on the first render - the bbox will be calculated on the next render - if (!this.isFirstRender && this.objects.size > 0) { + if (!this.isFirstRender && !this.renderer.hasObjects()) { // The layer is fully transparent but has objects - reset it this.manager.stateApi.onEntityReset({ id: this.id }, 'layer'); } @@ -297,67 +222,6 @@ export class CanvasLayer { }); }; - _renderObject = async (obj: CanvasLayerState['objects'][number], force = false): Promise => { - if (obj.type === 'brush_line') { - let brushLine = this.objects.get(obj.id); - assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); - - if (!brushLine) { - brushLine = new CanvasBrushLine(obj, this); - this.objects.set(brushLine.id, brushLine); - this.konva.objectGroup.add(brushLine.konva.group); - return true; - } else { - return await brushLine.update(obj, force); - } - } else if (obj.type === 'eraser_line') { - let eraserLine = this.objects.get(obj.id); - assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined); - - if (!eraserLine) { - eraserLine = new CanvasEraserLine(obj, this); - this.objects.set(eraserLine.id, eraserLine); - this.konva.objectGroup.add(eraserLine.konva.group); - return true; - } else { - if (await eraserLine.update(obj, force)) { - return true; - } - } - } else if (obj.type === 'rect') { - let rect = this.objects.get(obj.id); - assert(rect instanceof CanvasRect || rect === undefined); - - if (!rect) { - rect = new CanvasRect(obj, this); - this.objects.set(rect.id, rect); - this.konva.objectGroup.add(rect.konva.group); - return true; - } else { - if (await rect.update(obj, force)) { - return true; - } - } - } else if (obj.type === 'image') { - let image = this.objects.get(obj.id); - assert(image instanceof CanvasImage || image === undefined); - - if (!image) { - image = new CanvasImage(obj, this); - this.objects.set(image.id, image); - this.konva.objectGroup.add(image.konva.group); - await image.updateImageSource(obj.image.name); - return true; - } else { - if (await image.update(obj, force)) { - return true; - } - } - } - - return false; - }; - startTransform = () => { this.log.debug('Starting transform'); this.isTransforming = true; @@ -365,9 +229,8 @@ export class CanvasLayer { // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or // interaction rect are listening, it will interrupt the stage's drag events. So we should disable listening // when the view tool is selected - const listening = this.manager.stateApi.getToolState().selected !== 'view'; - - this.konva.layer.listening(listening); + const shouldListen = this.manager.stateApi.getToolState().selected !== 'view'; + this.konva.layer.listening(shouldListen); this.transformer.setMode('transform'); }; @@ -395,12 +258,8 @@ export class CanvasLayer { const imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true); const { dispatch } = getStore(); const imageObject = imageDTOToImageObject(imageDTO); - await this._renderObject(imageObject, true); - for (const obj of this.objects.values()) { - if (obj.id !== imageObject.id) { - obj.konva.group.visible(false); - } - } + await this.renderer.renderObject(imageObject, true); + this.renderer.hideAll([imageObject.id]); this.resetScale(); dispatch(layerRasterized({ id: this.id, imageObject, position: { x: Math.round(rect.x), y: Math.round(rect.y) } })); }; @@ -424,7 +283,7 @@ export class CanvasLayer { this.isPendingBboxCalculation = true; - if (this.objects.size === 0) { + if (!this.renderer.hasObjects()) { this.log.trace('No objects, resetting bbox'); this.rect = this.getDefaultRect(); this.bbox = this.getDefaultRect(); @@ -435,30 +294,7 @@ export class CanvasLayer { const rect = this.konva.objectGroup.getClientRect({ skipTransform: true }); - /** - * In some cases, we can use konva's getClientRect as the bbox, but there are some cases where we need to calculate - * the bbox using pixel data: - * - * - Eraser lines are normal lines, except they composite as transparency. Konva's getClientRect includes them when - * calculating the bbox. - * - Clipped portions of lines will be included in the client rect. - * - Images have transparency, so they will be included in the client rect. - * - * TODO(psyche): Using pixel data is slow. Is it possible to be clever and somehow subtract the eraser lines and - * clipped areas from the client rect? - */ - let needsPixelBbox = false; - for (const obj of this.objects.values()) { - const isEraserLine = obj instanceof CanvasEraserLine; - const isImage = obj instanceof CanvasImage; - const hasClip = obj instanceof CanvasBrushLine && obj.state.clip; - if (isEraserLine || hasClip || isImage) { - needsPixelBbox = true; - break; - } - } - - if (!needsPixelBbox) { + if (!this.renderer.needsPixelBbox()) { this.rect = deepClone(rect); this.bbox = deepClone(rect); this.isPendingBboxCalculation = false; @@ -508,10 +344,10 @@ export class CanvasLayer { rect: deepClone(this.rect), bbox: deepClone(this.bbox), bboxNeedsUpdate: this.bboxNeedsUpdate, - isFirstRender: this.isFirstRender, isTransforming: this.isTransforming, isPendingBboxCalculation: this.isPendingBboxCalculation, - objects: Array.from(this.objects.values()).map((obj) => obj.repr()), + transformer: this.transformer.repr(), + renderer: this.renderer.repr(), }; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 177236b88a..ffb1678376 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -2,12 +2,13 @@ import type { Store } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import type { JSONObject } from 'common/types'; -import type { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine'; -import type { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'; -import type { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; +import type { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine'; +import type { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; +import type { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import { CanvasInitialImage } from 'features/controlLayers/konva/CanvasInitialImage'; +import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview'; -import type { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; +import type { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; import type { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import { getCompositeLayerImage, @@ -593,11 +594,12 @@ export class CanvasManager { buildGetLoggingContext = ( instance: - | CanvasBrushLine - | CanvasEraserLine - | CanvasRect - | CanvasImage + | CanvasBrushLineRenderer + | CanvasEraserLineRenderer + | CanvasRectRenderer + | CanvasImageRenderer | CanvasTransformer + | CanvasObjectRenderer | CanvasLayer | CanvasStagingArea ): GetLoggingContext => { @@ -609,6 +611,14 @@ export class CanvasManager { ...extra, }; }; + } else if (instance instanceof CanvasObjectRenderer) { + return (extra?: JSONObject): JSONObject => { + return { + ...instance.parent.getLoggingContext(), + rendererId: instance.id, + ...extra, + }; + }; } else { return (extra?: JSONObject): JSONObject => { return { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts new file mode 100644 index 0000000000..559f21cd90 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -0,0 +1,215 @@ +import type { JSONObject } from 'common/types'; +import { deepClone } from 'common/util/deepClone'; +import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine'; +import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; +import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; +import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; +import type { + CanvasBrushLineState, + CanvasEraserLineState, + CanvasImageState, + CanvasRectState, +} from 'features/controlLayers/store/types'; +import type { Logger } from 'roarr'; +import { assert } from 'tsafe'; + +type AnyObjectRenderer = CanvasBrushLineRenderer | CanvasEraserLineRenderer | CanvasRectRenderer | CanvasImageRenderer; +type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImageState | CanvasRectState; + +export class CanvasObjectRenderer { + static TYPE = 'object_renderer'; + static OBJECT_GROUP_NAME = `${CanvasObjectRenderer.TYPE}_group`; + + id: string; + parent: CanvasLayer; + manager: CanvasManager; + log: Logger; + getLoggingContext: (extra?: JSONObject) => JSONObject; + + isFirstRender: boolean = true; + isRendering: boolean = false; + buffer: AnyObjectState | null = null; + renderers: Map = new Map(); + + constructor(parent: CanvasLayer) { + this.id = getPrefixedId(CanvasObjectRenderer.TYPE); + this.parent = parent; + this.manager = parent.manager; + this.getLoggingContext = this.manager.buildGetLoggingContext(this); + this.log = this.manager.buildLogger(this.getLoggingContext); + this.log.trace('Creating object renderer'); + } + + render = async (objectStates: AnyObjectState[]): Promise => { + this.isRendering = true; + let didRender = false; + const objectIds = objectStates.map((objectState) => objectState.id); + + for (const renderer of this.renderers.values()) { + if (!objectIds.includes(renderer.id) && renderer.id !== this.buffer?.id) { + this.renderers.delete(renderer.id); + renderer.destroy(); + didRender = true; + } + } + + for (const objectState of objectStates) { + didRender = (await this.renderObject(objectState)) || didRender; + } + + if (this.buffer) { + didRender = (await this.renderObject(this.buffer)) || didRender; + } + + this.isRendering = false; + this.isFirstRender = false; + + return didRender; + }; + + renderObject = async (objectState: AnyObjectState, force?: boolean): Promise => { + let didRender = false; + + if (objectState.type === 'brush_line') { + let renderer = this.renderers.get(objectState.id); + assert(renderer instanceof CanvasBrushLineRenderer || renderer === undefined); + + if (!renderer) { + renderer = new CanvasBrushLineRenderer(objectState, this); + this.renderers.set(renderer.id, renderer); + this.parent.konva.objectGroup.add(renderer.konva.group); + } + + didRender = renderer.update(objectState, force); + } else if (objectState.type === 'eraser_line') { + let renderer = this.renderers.get(objectState.id); + assert(renderer instanceof CanvasEraserLineRenderer || renderer === undefined); + + if (!renderer) { + renderer = new CanvasEraserLineRenderer(objectState, this); + this.renderers.set(renderer.id, renderer); + this.parent.konva.objectGroup.add(renderer.konva.group); + } + + didRender = renderer.update(objectState, force); + } else if (objectState.type === 'rect') { + let renderer = this.renderers.get(objectState.id); + assert(renderer instanceof CanvasRectRenderer || renderer === undefined); + + if (!renderer) { + renderer = new CanvasRectRenderer(objectState, this); + this.renderers.set(renderer.id, renderer); + this.parent.konva.objectGroup.add(renderer.konva.group); + } + + didRender = renderer.update(objectState, force); + } else if (objectState.type === 'image') { + let renderer = this.renderers.get(objectState.id); + assert(renderer instanceof CanvasImageRenderer || renderer === undefined); + + if (!renderer) { + renderer = new CanvasImageRenderer(objectState, this); + this.renderers.set(renderer.id, renderer); + this.parent.konva.objectGroup.add(renderer.konva.group); + } + didRender = await renderer.update(objectState, force); + } + + this.isFirstRender = false; + return didRender; + }; + + hasBuffer = (): boolean => { + return this.buffer !== null; + }; + + setBuffer = async (objectState: AnyObjectState): Promise => { + this.buffer = objectState; + return await this.renderObject(this.buffer, true); + }; + + clearBuffer = () => { + this.buffer = null; + }; + + commitBuffer = () => { + if (!this.buffer) { + return; + } + + // We need to give the objects a fresh ID else they will be considered the same object when they are re-rendered as + // a non-buffer object, and we won't trigger things like bbox calculation + this.buffer.id = getPrefixedId(this.buffer.type); + + if (this.buffer.type === 'brush_line') { + this.manager.stateApi.onBrushLineAdded({ id: this.parent.id, brushLine: this.buffer }, 'layer'); + } else if (this.buffer.type === 'eraser_line') { + this.manager.stateApi.onEraserLineAdded({ id: this.parent.id, eraserLine: this.buffer }, 'layer'); + } else if (this.buffer.type === 'rect') { + this.manager.stateApi.onRectShapeAdded({ id: this.parent.id, rectShape: this.buffer }, 'layer'); + } else { + this.log.warn({ buffer: this.buffer }, 'Invalid buffer object type'); + } + + this.buffer = null; + }; + + /** + * Determines if the objects in the renderer require a pixel bbox calculation. + * + * In some cases, we can use Konva's getClientRect as the bbox, but it is not always accurate. It includes + * these visually transparent shapes in its calculation: + * + * - Eraser lines, which are normal lines with a globalCompositeOperation of 'destination-out'. + * - Clipped portions of any shape. + * - Images, which may have transparent areas. + */ + needsPixelBbox = (): boolean => { + let needsPixelBbox = false; + for (const renderer of this.renderers.values()) { + const isEraserLine = renderer instanceof CanvasEraserLineRenderer; + const isImage = renderer instanceof CanvasImageRenderer; + const hasClip = renderer instanceof CanvasBrushLineRenderer && renderer.state.clip; + if (isEraserLine || hasClip || isImage) { + needsPixelBbox = true; + break; + } + } + return needsPixelBbox; + }; + + hasObjects = (): boolean => { + return this.renderers.size > 0 || this.buffer !== null; + }; + + hideAll = (except: string[]) => { + for (const renderer of this.renderers.values()) { + if (!except.includes(renderer.id)) { + renderer.setVisibility(false); + } + } + }; + + destroy = () => { + this.log.trace('Destroying object renderer'); + for (const renderer of this.renderers.values()) { + renderer.destroy(); + } + this.renderers.clear(); + }; + + repr = () => { + return { + id: this.id, + type: CanvasObjectRenderer.TYPE, + parent: this.parent.id, + renderers: Array.from(this.renderers.values()).map((renderer) => renderer.repr()), + buffer: deepClone(this.buffer), + isFirstRender: this.isFirstRender, + isRendering: this.isRendering, + }; + }; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index c714a60144..32a91a6a9a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -1,18 +1,18 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; -import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { GetLoggingContext, CanvasRectState } from 'features/controlLayers/store/types'; +import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; +import type { CanvasRectState, GetLoggingContext } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasRect { +export class CanvasRectRenderer { static TYPE = 'rect'; - static GROUP_NAME = `${CanvasRect.TYPE}_group`; - static RECT_NAME = `${CanvasRect.TYPE}_rect`; + static GROUP_NAME = `${CanvasRectRenderer.TYPE}_group`; + static RECT_NAME = `${CanvasRectRenderer.TYPE}_rect`; id: string; - parent: CanvasLayer; + parent: CanvasObjectRenderer; manager: CanvasManager; log: Logger; getLoggingContext: GetLoggingContext; @@ -22,8 +22,9 @@ export class CanvasRect { group: Konva.Group; rect: Konva.Rect; }; + isFirstRender: boolean = false; - constructor(state: CanvasRectState, parent: CanvasLayer) { + constructor(state: CanvasRectState, parent: CanvasObjectRenderer) { const { id, x, y, width, height, color } = state; this.id = id; this.parent = parent; @@ -33,9 +34,9 @@ export class CanvasRect { this.log.trace({ state }, 'Creating rect'); this.konva = { - group: new Konva.Group({ name: CanvasRect.GROUP_NAME, listening: false }), + group: new Konva.Group({ name: CanvasRectRenderer.GROUP_NAME, listening: false }), rect: new Konva.Rect({ - name: CanvasRect.RECT_NAME, + name: CanvasRectRenderer.RECT_NAME, x, y, width, @@ -48,8 +49,10 @@ export class CanvasRect { this.state = state; } - update(state: CanvasRectState, force?: boolean): boolean { + update(state: CanvasRectState, force = this.isFirstRender): boolean { if (this.state !== state || force) { + this.isFirstRender = false; + this.log.trace({ state }, 'Updating rect'); const { x, y, width, height, color } = state; this.konva.rect.setAttrs({ @@ -61,9 +64,9 @@ export class CanvasRect { }); this.state = state; return true; - } else { - return false; } + + return false; } destroy() { @@ -79,8 +82,9 @@ export class CanvasRect { repr() { return { id: this.id, - type: CanvasRect.TYPE, + type: CanvasRectRenderer.TYPE, parent: this.parent.id, + isFirstRender: this.isFirstRender, state: deepClone(this.state), }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts index 30851a79e4..c48f95d851 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts @@ -1,8 +1,8 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine'; -import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'; +import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine'; +import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; +import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; import { mapId } from 'features/controlLayers/konva/util'; import type { CanvasBrushLineState, CanvasEraserLineState, CanvasRectState, CanvasRegionalGuidanceState } from 'features/controlLayers/store/types'; @@ -32,7 +32,7 @@ export class CanvasRegion { transformer: Konva.Transformer; }; - objects: Map; + objects: Map; constructor(state: CanvasRegionalGuidanceState, manager: CanvasManager) { this.id = state.id; @@ -155,10 +155,10 @@ export class CanvasRegion { private async renderObject(obj: CanvasRegionalGuidanceState['objects'][number], force = false): Promise { if (obj.type === 'brush_line') { let brushLine = this.objects.get(obj.id); - assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); + assert(brushLine instanceof CanvasBrushLineRenderer || brushLine === undefined); if (!brushLine) { - brushLine = new CanvasBrushLine(obj); + brushLine = new CanvasBrushLineRenderer(obj); this.objects.set(brushLine.id, brushLine); this.konva.objectGroup.add(brushLine.konva.group); return true; @@ -169,10 +169,10 @@ export class CanvasRegion { } } else if (obj.type === 'eraser_line') { let eraserLine = this.objects.get(obj.id); - assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined); + assert(eraserLine instanceof CanvasEraserLineRenderer || eraserLine === undefined); if (!eraserLine) { - eraserLine = new CanvasEraserLine(obj); + eraserLine = new CanvasEraserLineRenderer(obj); this.objects.set(eraserLine.id, eraserLine); this.konva.objectGroup.add(eraserLine.konva.group); return true; @@ -183,10 +183,10 @@ export class CanvasRegion { } } else if (obj.type === 'rect') { let rect = this.objects.get(obj.id); - assert(rect instanceof CanvasRect || rect === undefined); + assert(rect instanceof CanvasRectRenderer || rect === undefined); if (!rect) { - rect = new CanvasRect(obj); + rect = new CanvasRectRenderer(obj); this.objects.set(rect.id, rect); this.konva.objectGroup.add(rect.konva.group); return true; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index b881b5f7f9..64e763aa6c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -1,4 +1,4 @@ -import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; +import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { GetLoggingContext, StagingAreaImage } from 'features/controlLayers/store/types'; @@ -16,7 +16,7 @@ export class CanvasStagingArea { konva: { group: Konva.Group }; - image: CanvasImage | null; + image: CanvasImageRenderer | null; selectedImage: StagingAreaImage | null; constructor(manager: CanvasManager) { @@ -43,7 +43,7 @@ export class CanvasStagingArea { if (!this.image) { const { image_name, width, height } = imageDTO; - this.image = new CanvasImage( + this.image = new CanvasImageRenderer( { id: 'staging-area-image', type: 'image', diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index 81711a479d..25563bce23 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -46,22 +46,16 @@ export class CanvasTransformer { */ isTransformEnabled: boolean; - /** - * The konva group that the transformer will manipulate. - */ - transformTarget: Konva.Group; - konva: { transformer: Konva.Transformer; proxyRect: Konva.Rect; bboxOutline: Konva.Rect; }; - constructor(parent: CanvasLayer, transformTarget: Konva.Group) { + constructor(parent: CanvasLayer) { this.id = getPrefixedId(CanvasTransformer.TYPE); this.parent = parent; this.manager = parent.manager; - this.transformTarget = transformTarget; this.getLoggingContext = this.manager.buildGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); @@ -192,7 +186,7 @@ export class CanvasTransformer { // This is called when a transform anchor is dragged. By this time, the transform constraints in the above // callbacks have been enforced, and the transformer has updated its nodes' attributes. We need to pass the // updated attributes to the object group, propagating the transformation on down. - this.transformTarget.setAttrs({ + this.parent.konva.objectGroup.setAttrs({ x: this.konva.proxyRect.x(), y: this.konva.proxyRect.y(), scaleX: this.konva.proxyRect.scaleX(), @@ -234,7 +228,7 @@ export class CanvasTransformer { scaleX: snappedScaleX, scaleY: snappedScaleY, }); - this.transformTarget.setAttrs({ + this.parent.konva.objectGroup.setAttrs({ x: snappedX, y: snappedY, scaleX: snappedScaleX, @@ -278,7 +272,7 @@ export class CanvasTransformer { // The object group is translated by the difference between the interaction rect's new and old positions (which is // stored as this.bbox) - this.transformTarget.setAttrs({ + this.parent.konva.objectGroup.setAttrs({ x: this.konva.proxyRect.x(), y: this.konva.proxyRect.y(), }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index e4b2ee7d3f..9c07676a6b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -6,11 +6,11 @@ import { offsetCoord, } from 'features/controlLayers/konva/util'; import type { - CanvasV2State, - Coordinate, CanvasInpaintMaskState, CanvasLayerState, CanvasRegionalGuidanceState, + CanvasV2State, + Coordinate, Tool, } from 'features/controlLayers/store/types'; import { isDrawableEntity, isDrawableEntityAdapter } from 'features/controlLayers/store/types'; @@ -189,11 +189,11 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); if (e.evt.shiftKey && lastLinePoint) { // Create a straight line from the last line point - if (selectedEntityAdapter.getDrawingBuffer()) { - await selectedEntityAdapter.finalizeDrawingBuffer(); + if (selectedEntityAdapter.renderer.buffer) { + await selectedEntityAdapter.renderer.commitBuffer(); } - await selectedEntityAdapter.setDrawingBuffer({ + await selectedEntityAdapter.renderer.setBuffer({ id: getObjectId('brush_line', true), type: 'brush_line', points: [ @@ -208,10 +208,10 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { clip: getClip(selectedEntity), }); } else { - if (selectedEntityAdapter.getDrawingBuffer()) { - await selectedEntityAdapter.finalizeDrawingBuffer(); + if (selectedEntityAdapter.renderer.buffer) { + await selectedEntityAdapter.renderer.commitBuffer(); } - await selectedEntityAdapter.setDrawingBuffer({ + await selectedEntityAdapter.renderer.setBuffer({ id: getObjectId('brush_line', true), type: 'brush_line', points: [alignedPoint.x, alignedPoint.y], @@ -228,10 +228,10 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); if (e.evt.shiftKey && lastLinePoint) { // Create a straight line from the last line point - if (selectedEntityAdapter.getDrawingBuffer()) { - await selectedEntityAdapter.finalizeDrawingBuffer(); + if (selectedEntityAdapter.renderer.buffer) { + await selectedEntityAdapter.renderer.commitBuffer(); } - await selectedEntityAdapter.setDrawingBuffer({ + await selectedEntityAdapter.renderer.setBuffer({ id: getObjectId('eraser_line', true), type: 'eraser_line', points: [ @@ -245,10 +245,10 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { clip: getClip(selectedEntity), }); } else { - if (selectedEntityAdapter.getDrawingBuffer()) { - await selectedEntityAdapter.finalizeDrawingBuffer(); + if (selectedEntityAdapter.renderer.buffer) { + await selectedEntityAdapter.renderer.commitBuffer(); } - await selectedEntityAdapter.setDrawingBuffer({ + await selectedEntityAdapter.renderer.setBuffer({ id: getObjectId('eraser_line', true), type: 'eraser_line', points: [alignedPoint.x, alignedPoint.y], @@ -260,10 +260,10 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } if (toolState.selected === 'rect') { - if (selectedEntityAdapter.getDrawingBuffer()) { - await selectedEntityAdapter.finalizeDrawingBuffer(); + if (selectedEntityAdapter.renderer.buffer) { + await selectedEntityAdapter.renderer.commitBuffer(); } - await selectedEntityAdapter.setDrawingBuffer({ + await selectedEntityAdapter.renderer.setBuffer({ id: getObjectId('rect', true), type: 'rect', x: Math.round(normalizedPoint.x), @@ -295,29 +295,29 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const toolState = getToolState(); if (toolState.selected === 'brush') { - const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + const drawingBuffer = selectedEntityAdapter.renderer.buffer; if (drawingBuffer?.type === 'brush_line') { - await selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.renderer.commitBuffer(); } else { - await selectedEntityAdapter.setDrawingBuffer(null); + await selectedEntityAdapter.renderer.clearBuffer(); } } if (toolState.selected === 'eraser') { - const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + const drawingBuffer = selectedEntityAdapter.renderer.buffer; if (drawingBuffer?.type === 'eraser_line') { - await selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.renderer.commitBuffer(); } else { - await selectedEntityAdapter.setDrawingBuffer(null); + await selectedEntityAdapter.renderer.clearBuffer(); } } if (toolState.selected === 'rect') { - const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + const drawingBuffer = selectedEntityAdapter.renderer.buffer; if (drawingBuffer?.type === 'rect') { - await selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.renderer.commitBuffer(); } else { - await selectedEntityAdapter.setDrawingBuffer(null); + await selectedEntityAdapter.renderer.clearBuffer(); } } @@ -344,7 +344,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { getIsPrimaryMouseDown(e) ) { if (toolState.selected === 'brush') { - const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + const drawingBuffer = selectedEntityAdapter.renderer.buffer; if (drawingBuffer) { if (drawingBuffer?.type === 'brush_line') { const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points)); @@ -352,19 +352,19 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const normalizedPoint = offsetCoord(nextPoint, selectedEntity.position); const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); + await selectedEntityAdapter.renderer.setBuffer(drawingBuffer); setLastAddedPoint(alignedPoint); } } else { - await selectedEntityAdapter.setDrawingBuffer(null); + await selectedEntityAdapter.renderer.clearBuffer(); } } else { - if (selectedEntityAdapter.getDrawingBuffer()) { - await selectedEntityAdapter.finalizeDrawingBuffer(); + if (selectedEntityAdapter.renderer.buffer) { + await selectedEntityAdapter.renderer.commitBuffer(); } const normalizedPoint = offsetCoord(pos, selectedEntity.position); const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); - await selectedEntityAdapter.setDrawingBuffer({ + await selectedEntityAdapter.renderer.setBuffer({ id: getObjectId('brush_line', true), type: 'brush_line', points: [alignedPoint.x, alignedPoint.y], @@ -377,7 +377,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } if (toolState.selected === 'eraser') { - const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + const drawingBuffer = selectedEntityAdapter.renderer.buffer; if (drawingBuffer) { if (drawingBuffer.type === 'eraser_line') { const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points)); @@ -385,19 +385,19 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const normalizedPoint = offsetCoord(nextPoint, selectedEntity.position); const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); + await selectedEntityAdapter.renderer.setBuffer(drawingBuffer); setLastAddedPoint(alignedPoint); } } else { - await selectedEntityAdapter.setDrawingBuffer(null); + await selectedEntityAdapter.renderer.clearBuffer(); } } else { - if (selectedEntityAdapter.getDrawingBuffer()) { - await selectedEntityAdapter.finalizeDrawingBuffer(); + if (selectedEntityAdapter.renderer.buffer) { + await selectedEntityAdapter.renderer.commitBuffer(); } const normalizedPoint = offsetCoord(pos, selectedEntity.position); const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); - await selectedEntityAdapter.setDrawingBuffer({ + await selectedEntityAdapter.renderer.setBuffer({ id: getObjectId('eraser_line', true), type: 'eraser_line', points: [alignedPoint.x, alignedPoint.y], @@ -409,15 +409,15 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } if (toolState.selected === 'rect') { - const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + const drawingBuffer = selectedEntityAdapter.renderer.buffer; if (drawingBuffer) { if (drawingBuffer.type === 'rect') { const normalizedPoint = offsetCoord(pos, selectedEntity.position); drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x); drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y); - await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); + await selectedEntityAdapter.renderer.setBuffer(drawingBuffer); } else { - await selectedEntityAdapter.setDrawingBuffer(null); + await selectedEntityAdapter.renderer.clearBuffer(); } } } @@ -443,23 +443,23 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { !getSpaceKey() && getIsPrimaryMouseDown(e) ) { - const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + const drawingBuffer = selectedEntityAdapter.renderer.buffer; const normalizedPoint = offsetCoord(pos, selectedEntity.position); if (toolState.selected === 'brush' && drawingBuffer?.type === 'brush_line') { const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); - await selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.renderer.setBuffer(drawingBuffer); + await selectedEntityAdapter.renderer.commitBuffer(); } else if (toolState.selected === 'eraser' && drawingBuffer?.type === 'eraser_line') { const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); - await selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.renderer.setBuffer(drawingBuffer); + await selectedEntityAdapter.renderer.commitBuffer(); } else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect') { drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x); drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y); - await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); - await selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.renderer.setBuffer(drawingBuffer); + await selectedEntityAdapter.renderer.commitBuffer(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index b382e43127..350c998135 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,7 +1,6 @@ import { getImageDataTransparency } from 'common/util/arrayBuffer'; -import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { Coordinate, GenerationMode, Rect, CanvasObjectState, RgbaColor } from 'features/controlLayers/store/types'; +import type { CanvasObjectState, Coordinate, GenerationMode, Rect, RgbaColor } from 'features/controlLayers/store/types'; import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; @@ -414,8 +413,6 @@ export function getCompositeLayerStageClone(arg: { manager: CanvasManager }): Ko if (!layer) { console.log('deleting', konvaLayer); toDelete.push(konvaLayer); - } else { - konvaLayer.findOne(`.${CanvasLayer.GROUP_NAME}`)?.findOne(`.${CanvasLayer.BBOX_NAME}`)?.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 353527d598..a77e6255cd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -572,8 +572,16 @@ const zCanvasImageState = z.object({ }); export type CanvasImageState = z.infer; -const zCanvasObjectState = z.discriminatedUnion('type', [zCanvasImageState, zCanvasBrushLineState, zCanvasEraserLineState, zCanvasRectState]); +const zCanvasObjectState = z.discriminatedUnion('type', [ + zCanvasImageState, + zCanvasBrushLineState, + zCanvasEraserLineState, + zCanvasRectState, +]); export type CanvasObjectState = z.infer; +export function isCanvasBrushLineState(obj: CanvasObjectState): obj is CanvasBrushLineState { + return obj.type === 'brush_line'; +} export const zCanvasLayerState = z.object({ id: zId, @@ -603,7 +611,13 @@ export type IPAdapterConfig = Pick< >; const zMaskObject = z - .discriminatedUnion('type', [zOLD_VectorMaskLine, zOLD_VectorMaskRect, zCanvasBrushLineState, zCanvasEraserLineState, zCanvasRectState]) + .discriminatedUnion('type', [ + zOLD_VectorMaskLine, + zOLD_VectorMaskRect, + zCanvasBrushLineState, + zCanvasEraserLineState, + zCanvasRectState, + ]) .transform((val) => { // Migrate old vector mask objects to new format if (val.type === 'vector_mask_line') { @@ -713,7 +727,10 @@ const zCanvasT2IAdapteState = zCanvasControlAdapterStateBase.extend({ }); export type CanvasT2IAdapterState = z.infer; -export const zCanvasControlAdapterState = z.discriminatedUnion('adapterType', [zCanvasControlNetState, zCanvasT2IAdapteState]); +export const zCanvasControlAdapterState = z.discriminatedUnion('adapterType', [ + zCanvasControlNetState, + zCanvasT2IAdapteState, +]); export type CanvasControlAdapterState = z.infer; export type ControlNetConfig = Pick< CanvasControlNetState, @@ -949,7 +966,9 @@ export type RemoveIndexString = { export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint'; -export function isDrawableEntity(entity: CanvasEntity): entity is CanvasLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState { +export function isDrawableEntity( + entity: CanvasEntity +): entity is CanvasLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState { return entity.type === 'layer' || entity.type === 'regional_guidance' || entity.type === 'inpaint_mask'; }