From 200338ed720ca957033b804044f70d3a65536cc6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:04:14 +1000 Subject: [PATCH] feat(ui): revised logging and naming setup, fix staging area --- .../addCommitStagingAreaImageListener.ts | 15 +++- .../controlLayers/konva/CanvasBrushLine.ts | 35 ++++----- .../controlLayers/konva/CanvasEntity.ts | 27 +++++++ .../controlLayers/konva/CanvasEraserLine.ts | 35 ++++----- .../controlLayers/konva/CanvasImage.ts | 46 ++++++------ .../controlLayers/konva/CanvasLayer.ts | 69 +++++++++-------- .../controlLayers/konva/CanvasManager.ts | 38 ++++++++-- .../controlLayers/konva/CanvasObject.ts | 48 ++++++++++++ .../controlLayers/konva/CanvasRect.ts | 36 ++++----- .../controlLayers/konva/CanvasStagingArea.ts | 52 ++++++++----- .../controlLayers/konva/CanvasStateApi.ts | 3 + .../features/controlLayers/konva/events.ts | 17 ++--- .../features/controlLayers/konva/naming.ts | 10 +-- .../src/features/controlLayers/konva/util.ts | 21 +++++- .../controlLayers/store/canvasV2Slice.ts | 1 - .../store/controlAdaptersReducers.ts | 6 +- .../store/initialImageReducers.ts | 2 +- .../controlLayers/store/ipAdaptersReducers.ts | 10 +-- .../controlLayers/store/layersReducers.ts | 74 +++++++------------ .../src/features/controlLayers/store/types.ts | 11 +-- 20 files changed, 309 insertions(+), 247 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index dcd3d7f70c..6917c83a21 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -2,10 +2,12 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { $lastProgressEvent, - layerAddedFromStagingArea, + layerAdded, sessionStagingAreaImageAccepted, sessionStagingAreaReset, } from 'features/controlLayers/store/canvasV2Slice'; +import type { LayerEntity } from 'features/controlLayers/store/types'; +import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { queueApi } from 'services/api/endpoints/queue'; @@ -50,7 +52,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { startAppListening({ actionCreator: sessionStagingAreaImageAccepted, - effect: async (action, api) => { + effect: (action, api) => { const { index } = action.payload; const state = api.getState(); const stagingAreaImage = state.canvasV2.session.stagedImages[index]; @@ -58,7 +60,14 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { assert(stagingAreaImage, 'No staged image found to accept'); const { x, y } = state.canvasV2.bbox.rect; - api.dispatch(layerAddedFromStagingArea({ stagingAreaImage, position: { x, y } })); + const { imageDTO, offsetX, offsetY } = stagingAreaImage; + const imageObject = imageDTOToImageObject(imageDTO); + const overrides: Partial<LayerEntity> = { + position: { x: x + offsetX, y: y + offsetY }, + objects: [imageObject], + }; + + api.dispatch(layerAdded({ overrides })); api.dispatch(sessionStagingAreaReset()); }, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index 794ce82b17..5d4d415917 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -1,32 +1,27 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; +import { CanvasObject } from 'features/controlLayers/konva/CanvasObject'; import type { BrushLine } from 'features/controlLayers/store/types'; import Konva from 'konva'; -export class CanvasBrushLine { +export class CanvasBrushLine extends CanvasObject { static NAME_PREFIX = 'brush-line'; static GROUP_NAME = `${CanvasBrushLine.NAME_PREFIX}_group`; static LINE_NAME = `${CanvasBrushLine.NAME_PREFIX}_line`; + static TYPE = 'brush_line'; state: BrushLine; - - type = 'brush_line'; - id: string; konva: { group: Konva.Group; line: Konva.Line; }; - parent: CanvasLayer; - constructor(state: BrushLine, parent: CanvasLayer) { - const { id, strokeWidth, clip, color, points } = state; + super(state.id, parent); + this._log.trace({ state }, 'Creating brush line'); - this.id = id; - - this.parent = parent; - this.parent._log.trace(`Creating brush line ${this.id}`); + const { strokeWidth, clip, color, points } = state; this.konva = { group: new Konva.Group({ @@ -36,7 +31,6 @@ export class CanvasBrushLine { }), line: new Konva.Line({ name: CanvasBrushLine.LINE_NAME, - id, listening: false, shadowForStrokeEnabled: false, strokeWidth, @@ -55,7 +49,7 @@ export class CanvasBrushLine { update(state: BrushLine, force?: boolean): boolean { if (force || this.state !== state) { - this.parent._log.trace(`Updating brush line ${this.id}`); + this._log.trace({ state }, 'Updating brush line'); const { points, color, clip, strokeWidth } = state; this.konva.line.setAttrs({ // A line with only one point will not be rendered, so we duplicate the points to make it visible @@ -72,23 +66,20 @@ export class CanvasBrushLine { } destroy() { - this.parent._log.trace(`Destroying brush line ${this.id}`); + this._log.trace('Destroying brush line'); this.konva.group.destroy(); } - show() { - this.konva.group.visible(true); - } - - hide() { - this.konva.group.visible(false); + setVisibility(isVisible: boolean): void { + this._log.trace({ isVisible }, 'Setting brush line visibility'); + this.konva.group.visible(isVisible); } repr() { return { id: this.id, - type: this.type, - parent: this.parent.id, + type: CanvasBrushLine.TYPE, + parent: this._parent.id, state: deepClone(this.state), }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity.ts new file mode 100644 index 0000000000..390d17d5cc --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity.ts @@ -0,0 +1,27 @@ +import type { JSONObject } from 'common/types'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { Logger } from 'roarr'; + +export abstract class CanvasEntity { + id: string; + _manager: CanvasManager; + _log: Logger; + + constructor(id: string, manager: CanvasManager) { + this.id = id; + this._manager = manager; + this._log = this._manager.buildLogger(this._getLoggingContext); + } + /** + * Get a serializable representation of the entity. + */ + abstract repr(): JSONObject; + + _getLoggingContext = (extra?: Record<string, unknown>) => { + return { + ...this._manager._getLoggingContext(), + layerId: this.id, + ...extra, + }; + }; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index 7ba26b02f9..dfe1ee5708 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -1,33 +1,28 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; +import { CanvasObject } from 'features/controlLayers/konva/CanvasObject'; import type { EraserLine } from 'features/controlLayers/store/types'; import { RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; -export class CanvasEraserLine { +export class CanvasEraserLine extends CanvasObject { static NAME_PREFIX = 'eraser-line'; static GROUP_NAME = `${CanvasEraserLine.NAME_PREFIX}_group`; static LINE_NAME = `${CanvasEraserLine.NAME_PREFIX}_line`; + static TYPE = 'eraser_line'; state: EraserLine; - - type = 'eraser_line'; - id: string; konva: { group: Konva.Group; line: Konva.Line; }; - parent: CanvasLayer; - constructor(state: EraserLine, parent: CanvasLayer) { - const { id, strokeWidth, clip, points } = state; + super(state.id, parent); + this._log.trace({ state }, 'Creating eraser line'); - this.id = id; - - this.parent = parent; - this.parent._log.trace(`Creating eraser line ${this.id}`); + const { strokeWidth, clip, points } = state; this.konva = { group: new Konva.Group({ @@ -37,7 +32,6 @@ export class CanvasEraserLine { }), line: new Konva.Line({ name: CanvasEraserLine.LINE_NAME, - id, listening: false, shadowForStrokeEnabled: false, strokeWidth, @@ -56,7 +50,7 @@ export class CanvasEraserLine { update(state: EraserLine, force?: boolean): boolean { if (force || this.state !== state) { - this.parent._log.trace(`Updating eraser line ${this.id}`); + this._log.trace({ state }, 'Updating eraser line'); const { points, clip, strokeWidth } = state; this.konva.line.setAttrs({ // A line with only one point will not be rendered, so we duplicate the points to make it visible @@ -72,23 +66,20 @@ export class CanvasEraserLine { } destroy() { - this.parent._log.trace(`Destroying eraser line ${this.id}`); + this._log.trace('Destroying eraser line'); this.konva.group.destroy(); } - show() { - this.konva.group.visible(true); - } - - hide() { - this.konva.group.visible(false); + setVisibility(isVisible: boolean): void { + this._log.trace({ isVisible }, 'Setting brush line visibility'); + this.konva.group.visible(isVisible); } repr() { return { id: this.id, - type: this.type, - parent: this.parent.id, + type: CanvasEraserLine.TYPE, + parent: this._parent.id, 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 9fb722a3bc..96c14ba27d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -1,26 +1,24 @@ import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; +import { CanvasObject } from 'features/controlLayers/konva/CanvasObject'; +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 { ImageObject } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; import { getImageDTO } from 'services/api/endpoints/images'; -import { assert } from 'tsafe'; -export class CanvasImage { +export class CanvasImage extends CanvasObject { static NAME_PREFIX = 'canvas-image'; static GROUP_NAME = `${CanvasImage.NAME_PREFIX}_group`; static IMAGE_NAME = `${CanvasImage.NAME_PREFIX}_image`; static PLACEHOLDER_GROUP_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-group`; static PLACEHOLDER_RECT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-rect`; static PLACEHOLDER_TEXT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-text`; + static TYPE = 'image'; state: ImageObject; - - type = 'image'; - - id: string; konva: { group: Konva.Group; placeholder: { group: Konva.Group; rect: Konva.Rect; text: Konva.Text }; @@ -30,14 +28,11 @@ export class CanvasImage { isLoading: boolean; isError: boolean; - parent: CanvasLayer; + constructor(state: ImageObject, parent: CanvasLayer | CanvasStagingArea) { + super(state.id, parent); + this._log.trace({ state }, 'Creating image'); - constructor(state: ImageObject, parent: CanvasLayer) { - const { id, width, height, x, y } = state; - this.id = id; - - this.parent = parent; - this.parent._log.trace(`Creating image ${this.id}`); + const { width, height, x, y } = state; this.konva = { group: new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }), @@ -78,7 +73,7 @@ export class CanvasImage { async updateImageSource(imageName: string) { try { - this.parent._log.trace(`Updating image source ${this.id}`); + this._log.trace({ imageName }, 'Updating image source'); this.isLoading = true; this.konva.group.visible(true); @@ -89,7 +84,10 @@ export class CanvasImage { } const imageDTO = await getImageDTO(imageName); - assert(imageDTO !== null, 'imageDTO is null'); + if (imageDTO === null) { + this._log.error({ imageName }, 'Image not found'); + return; + } const imageEl = await loadImage(imageDTO.image_url); if (this.konva.image) { @@ -120,6 +118,7 @@ export class CanvasImage { this.isError = false; this.konva.placeholder.group.visible(false); } catch { + this._log({ imageName }, 'Failed to load image'); this.konva.image?.visible(false); this.imageName = null; this.isLoading = false; @@ -131,7 +130,7 @@ export class CanvasImage { async update(state: ImageObject, force?: boolean): Promise<boolean> { if (this.state !== state || force) { - this.parent._log.trace(`Updating image ${this.id}`); + this._log.trace({ state }, 'Updating image'); const { width, height, x, y, image, filters } = state; if (this.state.image.name !== image.name || force) { @@ -155,23 +154,20 @@ export class CanvasImage { } destroy() { - this.parent._log.trace(`Destroying image ${this.id}`); + this._log.trace('Destroying image'); this.konva.group.destroy(); } - show() { - this.konva.group.visible(true); - } - - hide() { - this.konva.group.visible(false); + setVisibility(isVisible: boolean): void { + this._log.trace({ isVisible }, 'Setting image visibility'); + this.konva.group.visible(isVisible); } repr() { return { id: this.id, - type: this.type, - parent: this.parent.id, + type: CanvasImage.TYPE, + parent: this._parent.id, imageName: this.imageName, isLoading: this.isLoading, isError: this.isError, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 7cf905053c..665d7daacd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -1,12 +1,12 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine'; +import { CanvasEntity } from 'features/controlLayers/konva/CanvasEntity'; 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 { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; -import { konvaNodeToBlob, mapId, nanoid, previewBlob } from 'features/controlLayers/konva/util'; +import { getPrefixedId, konvaNodeToBlob, mapId, previewBlob } from 'features/controlLayers/konva/util'; import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice'; import { type BrushLine, @@ -20,11 +20,10 @@ import { } from 'features/controlLayers/store/types'; 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 { +export class CanvasLayer extends CanvasEntity { static NAME_PREFIX = 'layer'; static LAYER_NAME = `${CanvasLayer.NAME_PREFIX}_layer`; static TRANSFORMER_NAME = `${CanvasLayer.NAME_PREFIX}_transformer`; @@ -36,8 +35,7 @@ export class CanvasLayer { _drawingBuffer: BrushLine | EraserLine | RectShape | null; _state: LayerEntity; - id: string; - manager: CanvasManager; + type = 'layer'; konva: { layer: Konva.Layer; @@ -48,7 +46,6 @@ export class CanvasLayer { }; objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage>; - _log: Logger; _bboxNeedsUpdate: boolean; _isFirstRender: boolean; @@ -59,8 +56,9 @@ export class CanvasLayer { bbox: Rect; constructor(state: LayerEntity, manager: CanvasManager) { - this.id = state.id; - this.manager = manager; + super(state.id, manager); + this._log.debug({ state }, 'Creating layer'); + this.konva = { layer: new Konva.Layer({ id: this.id, name: CanvasLayer.LAYER_NAME, listening: false }), bbox: new Konva.Rect({ @@ -79,7 +77,7 @@ export class CanvasLayer { rotateEnabled: true, flipEnabled: true, listening: false, - padding: this.manager.getTransformerPadding(), + padding: this._manager.getTransformerPadding(), stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400 keepRatio: false, }), @@ -149,8 +147,8 @@ export class CanvasLayer { // The bbox should be updated to reflect the new position of the interaction rect, taking into account its padding // and border this.konva.bbox.setAttrs({ - x: this.konva.interactionRect.x() - this.manager.getScaledBboxPadding(), - y: this.konva.interactionRect.y() - this.manager.getScaledBboxPadding(), + x: this.konva.interactionRect.x() - this._manager.getScaledBboxPadding(), + y: this.konva.interactionRect.y() - this._manager.getScaledBboxPadding(), }); // The object group is translated by the difference between the interaction rect's new and old positions (which is @@ -169,7 +167,7 @@ export class CanvasLayer { return; } - this.manager.stateApi.onPosChanged( + this._manager.stateApi.onPosChanged( { id: this.id, position: { @@ -190,11 +188,10 @@ export class CanvasLayer { this.isTransforming = false; this._isFirstRender = true; this.isPendingBboxCalculation = false; - this._log = this.manager.getLogger(`layer_${this.id}`); } destroy(): void { - this._log.debug(`Layer ${this.id} - destroying`); + this._log.debug('Destroying layer'); this.konva.layer.destroy(); } @@ -221,21 +218,21 @@ export class CanvasLayer { // a non-buffer object, and we won't trigger things like bbox calculation if (drawingBuffer.type === 'brush_line') { - drawingBuffer.id = getBrushLineId(this.id, nanoid()); - this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: drawingBuffer }, 'layer'); + drawingBuffer.id = getPrefixedId('brush_line'); + this._manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: drawingBuffer }, 'layer'); } else if (drawingBuffer.type === 'eraser_line') { - drawingBuffer.id = getEraserLineId(this.id, nanoid()); - this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: drawingBuffer }, 'layer'); + drawingBuffer.id = getPrefixedId('brush_line'); + this._manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: drawingBuffer }, 'layer'); } else if (drawingBuffer.type === 'rect_shape') { - drawingBuffer.id = getRectShapeId(this.id, nanoid()); - this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: drawingBuffer }, 'layer'); + drawingBuffer.id = getPrefixedId('brush_line'); + this._manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: drawingBuffer }, 'layer'); } } async update(arg?: { state: LayerEntity; toolState: CanvasV2State['tool']; isSelected: boolean }) { const state = get(arg, 'state', this._state); - const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState()); - const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id)); + const toolState = get(arg, 'toolState', this._manager.stateApi.getToolState()); + const isSelected = get(arg, 'isSelected', this._manager.stateApi.getIsSelected(this.id)); if (!this._isFirstRender && state === this._state) { this._log.trace('State unchanged, skipping update'); @@ -277,7 +274,7 @@ export class CanvasLayer { updatePosition(arg?: { position: Coordinate }) { this._log.trace('Updating position'); const position = get(arg, 'position', this._state.position); - const bboxPadding = this.manager.getScaledBboxPadding(); + const bboxPadding = this._manager.getScaledBboxPadding(); this.konva.objectGroup.setAttrs({ x: position.x + this.bbox.x, @@ -339,8 +336,8 @@ export class CanvasLayer { updateInteraction(arg?: { toolState: CanvasV2State['tool']; isSelected: boolean }) { this._log.trace('Updating interaction'); - const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState()); - const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id)); + 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) { // The layer is totally empty, we can just disable the layer @@ -397,7 +394,7 @@ export class CanvasLayer { if (this.bbox.width === 0 || this.bbox.height === 0) { if (this.objects.size > 0) { // The layer is fully transparent but has objects - reset it - this.manager.stateApi.onEntityReset({ id: this.id }, 'layer'); + this._manager.stateApi.onEntityReset({ id: this.id }, 'layer'); } this.konva.bbox.visible(false); this.konva.interactionRect.visible(false); @@ -407,8 +404,8 @@ export class CanvasLayer { this.konva.bbox.visible(true); this.konva.interactionRect.visible(true); - const onePixel = this.manager.getScaledPixel(); - const bboxPadding = this.manager.getScaledBboxPadding(); + const onePixel = this._manager.getScaledPixel(); + const bboxPadding = this._manager.getScaledBboxPadding(); this.konva.bbox.setAttrs({ x: this._state.position.x + this.bbox.x - bboxPadding, @@ -434,8 +431,8 @@ export class CanvasLayer { syncStageScale() { this._log.trace('Syncing scale to stage'); - const onePixel = this.manager.getScaledPixel(); - const bboxPadding = this.manager.getScaledBboxPadding(); + const onePixel = this._manager.getScaledPixel(); + const bboxPadding = this._manager.getScaledBboxPadding(); this.konva.bbox.setAttrs({ x: this.konva.interactionRect.x() - bboxPadding, @@ -515,7 +512,7 @@ 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'; + const listening = this._manager.stateApi.getToolState().selected !== 'view'; this.konva.layer.listening(listening); this.konva.interactionRect.listening(listening); @@ -546,12 +543,12 @@ export class CanvasLayer { const interactionRectClone = this.konva.interactionRect.clone(); const rect = interactionRectClone.getClientRect(); const blob = await konvaNodeToBlob(objectGroupClone, rect); - if (this.manager._isDebugging) { + if (this._manager._isDebugging) { previewBlob(blob, 'Rasterized layer'); } const imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true); const { dispatch } = getStore(); - const imageObject = imageDTOToImageObject(this.id, nanoid(), imageDTO); + const imageObject = imageDTOToImageObject(imageDTO); await this._renderObject(imageObject, true); for (const obj of this.objects.values()) { if (obj.id !== imageObject.id) { @@ -632,7 +629,7 @@ export class CanvasLayer { return; } const imageData = ctx.getImageData(0, 0, rect.width, rect.height); - this.manager.requestBbox( + this._manager.requestBbox( { buffer: imageData.data.buffer, width: imageData.width, height: imageData.height }, (extents) => { this.rect = deepClone(rect); @@ -658,7 +655,7 @@ export class CanvasLayer { repr() { return { id: this.id, - type: 'layer', + type: this.type, state: deepClone(this._state), rect: deepClone(this.rect), bbox: deepClone(this.bbox), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 1fd787ce3b..5ddfc1b681 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -1,6 +1,7 @@ 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 { CanvasInitialImage } from 'features/controlLayers/konva/CanvasInitialImage'; import { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview'; import { @@ -107,7 +108,15 @@ export class CanvasManager { this._prevState = this.stateApi.getState(); this._isFirstRender = true; - this.log = logger('canvas'); + this.log = logger('canvas').child((message) => { + return { + ...message, + context: { + ...message.context, + ...this._getLoggingContext(), + }, + }; + }); this.workerLog = logger('worker'); this.util = { @@ -173,11 +182,6 @@ export class CanvasManager { this._isDebugging = false; } - getLogger(namespace: string) { - const managerNamespace = this.log.getContext().namespace; - return this.log.child({ namespace: `${managerNamespace}.${namespace}` }); - } - requestBbox(data: Omit<GetBboxTask['data'], 'id'>, onComplete: (extents: Extents | null) => void) { const id = nanoid(); const task: GetBboxTask = { @@ -330,7 +334,6 @@ export class CanvasManager { for (const canvasLayer of this.layers.values()) { if (!state.layers.entities.find((l) => l.id === canvasLayer.id)) { - this.log.debug(`Destroying deleted layer ${canvasLayer.id}`); await canvasLayer.destroy(); this.layers.delete(canvasLayer.id); } @@ -339,7 +342,6 @@ export class CanvasManager { for (const entityState of state.layers.entities) { let adapter = this.layers.get(entityState.id); if (!adapter) { - this.log.debug(`Creating layer layer ${entityState.id}`); adapter = new CanvasLayer(entityState, this); this.layers.set(adapter.id, adapter); this.stage.add(adapter.konva.layer); @@ -562,9 +564,29 @@ export class CanvasManager { } } + _getLoggingContext() { + return { + // timestamp: new Date().toISOString(), + }; + } + + buildLogger(getContext: () => JSONObject): Logger { + return this.log.child((message) => { + return { + ...message, + context: { + ...message.context, + ...getContext(), + }, + }; + }); + } + logDebugInfo() { + // eslint-disable-next-line no-console console.log(this); for (const layer of this.layers.values()) { + // eslint-disable-next-line no-console console.log(layer); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject.ts new file mode 100644 index 0000000000..3a07b77e83 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject.ts @@ -0,0 +1,48 @@ +import type { JSONObject } from 'common/types'; +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 { Logger } from 'roarr'; + +export abstract class CanvasObject { + id: string; + + _parent: CanvasLayer | CanvasStagingArea; + _manager: CanvasManager; + _log: Logger; + + constructor(id: string, parent: CanvasLayer | CanvasStagingArea) { + this.id = id; + this._parent = parent; + this._manager = parent._manager; + this._log = this._manager.buildLogger(this._getLoggingContext); + } + + /** + * Destroy the object's konva nodes. + */ + abstract destroy(): void; + + /** + * Set the visibility of the object's konva nodes. + */ + abstract setVisibility(isVisible: boolean): void; + + /** + * Get a serializable representation of the object. + */ + abstract repr(): JSONObject; + + /** + * Get the logging context for this object. + * @param extra Extra data to merge into the context + * @returns The logging context for this object + */ + _getLoggingContext = (extra?: Record<string, unknown>) => { + return { + ...this._parent._getLoggingContext(), + objectId: this.id, + ...extra, + }; + }; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index 13d1ef1d65..96b4ac1c06 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -1,39 +1,32 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; +import { CanvasObject } from 'features/controlLayers/konva/CanvasObject'; import type { RectShape } from 'features/controlLayers/store/types'; import Konva from 'konva'; -export class CanvasRect { +export class CanvasRect extends CanvasObject { static NAME_PREFIX = 'canvas-rect'; static GROUP_NAME = `${CanvasRect.NAME_PREFIX}_group`; static RECT_NAME = `${CanvasRect.NAME_PREFIX}_rect`; + static TYPE = 'rect'; state: RectShape; - - type = 'rect'; - - id: string; konva: { group: Konva.Group; rect: Konva.Rect; }; - parent: CanvasLayer; - constructor(state: RectShape, parent: CanvasLayer) { - const { id, x, y, width, height, color } = state; + super(state.id, parent); + this._log.trace({ state }, 'Creating rect'); - this.id = id; - - this.parent = parent; - this.parent._log.trace(`Creating rect ${this.id}`); + const { x, y, width, height, color } = state; this.konva = { group: new Konva.Group({ name: CanvasRect.GROUP_NAME, listening: false }), rect: new Konva.Rect({ name: CanvasRect.RECT_NAME, - id, x, y, width, @@ -48,7 +41,7 @@ export class CanvasRect { update(state: RectShape, force?: boolean): boolean { if (this.state !== state || force) { - this.parent._log.trace(`Updating rect ${this.id}`); + this._log.trace({ state }, 'Updating rect'); const { x, y, width, height, color } = state; this.konva.rect.setAttrs({ x, @@ -65,23 +58,20 @@ export class CanvasRect { } destroy() { - this.parent._log.trace(`Destroying rect ${this.id}`); + this._log.trace('Destroying rect'); this.konva.group.destroy(); } - show() { - this.konva.group.visible(true); - } - - hide() { - this.konva.group.visible(false); + setVisibility(isVisible: boolean): void { + this._log.trace({ isVisible }, 'Setting rect visibility'); + this.konva.group.visible(isVisible); } repr() { return { id: this.id, - type: this.type, - parent: this.parent.id, + type: CanvasRect.TYPE, + parent: this._parent.id, state: deepClone(this.state), }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index cd4ae82e32..c7f83c5173 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -1,29 +1,30 @@ +import { CanvasEntity } from 'features/controlLayers/konva/CanvasEntity'; import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { StagingAreaImage } from 'features/controlLayers/store/types'; import Konva from 'konva'; -export class CanvasStagingArea { +export class CanvasStagingArea extends CanvasEntity { static NAME_PREFIX = 'staging-area'; static GROUP_NAME = `${CanvasStagingArea.NAME_PREFIX}_group`; + type = 'staging_area'; konva: { group: Konva.Group }; image: CanvasImage | null; selectedImage: StagingAreaImage | null; - manager: CanvasManager; constructor(manager: CanvasManager) { - this.manager = manager; + super('staging-area', manager); this.konva = { group: new Konva.Group({ name: CanvasStagingArea.GROUP_NAME, listening: false }) }; this.image = null; this.selectedImage = null; } async render() { - const session = this.manager.stateApi.getSession(); - const bboxRect = this.manager.stateApi.getBbox().rect; - const shouldShowStagedImage = this.manager.stateApi.getShouldShowStagedImage(); + const session = this._manager.stateApi.getSession(); + const bboxRect = this._manager.stateApi.getBbox().rect; + const shouldShowStagedImage = this._manager.stateApi.getShouldShowStagedImage(); this.selectedImage = session.stagedImages[session.selectedStagedImageIndex] ?? null; @@ -32,34 +33,45 @@ export class CanvasStagingArea { if (!this.image) { const { image_name, width, height } = imageDTO; - this.image = new CanvasImage({ - id: 'staging-area-image', - type: 'image', - x: 0, - y: 0, - width, - height, - filters: [], - image: { - name: image_name, + this.image = new CanvasImage( + { + id: 'staging-area-image', + type: 'image', + x: 0, + y: 0, width, height, + filters: [], + image: { + name: image_name, + width, + height, + }, }, - }); + this + ); this.konva.group.add(this.image.konva.group); } if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) { - this.image.image?.width(imageDTO.width); - this.image.image?.height(imageDTO.height); + 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); await this.image.updateImageSource(imageDTO.image_name); - this.manager.stateApi.resetLastProgressEvent(); + this._manager.stateApi.resetLastProgressEvent(); } this.image.konva.group.visible(shouldShowStagedImage); } else { this.image?.konva.group.visible(false); } } + + repr() { + return { + id: this.id, + type: this.type, + selectedImage: this.selectedImage, + }; + } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index e3d734a8a6..967b42ebca 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -248,6 +248,9 @@ export class CanvasStateApi { getIsSelected = (id: string) => { return this.getSelectedEntity()?.id === id; }; + getLogLevel = () => { + return this.store.getState().system.consoleLogLevel; + }; // Read-only state, derived from nanostores resetLastProgressEvent = () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 90d2917828..f590ea4939 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,5 +1,5 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { getScaledFlooredCursorPosition, nanoid } from 'features/controlLayers/konva/util'; +import { getObjectId, getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; import type { CanvasV2State, Coordinate, @@ -14,7 +14,6 @@ import type { KonvaEventObject } from 'konva/lib/Node'; import { clamp } from 'lodash-es'; import { BRUSH_SPACING_TARGET_SCALE, CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from './constants'; -import { getBrushLineId, getEraserLineId, getRectShapeId } from './naming'; /** * Updates the last cursor position atom with the current cursor position, returning the new position or `null` if the @@ -187,7 +186,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getBrushLineId(selectedEntityAdapter.id, nanoid(), true), + id: getObjectId('brush_line', true), type: 'brush_line', points: [ // The last point of the last line is already normalized to the entity's coordinates @@ -205,7 +204,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getBrushLineId(selectedEntityAdapter.id, nanoid(), true), + id: getObjectId('brush_line', true), type: 'brush_line', points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], strokeWidth: toolState.brush.width, @@ -224,7 +223,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getEraserLineId(selectedEntityAdapter.id, nanoid(), true), + id: getObjectId('eraser_line', true), type: 'eraser_line', points: [ // The last point of the last line is already normalized to the entity's coordinates @@ -241,7 +240,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getEraserLineId(selectedEntityAdapter.id, nanoid(), true), + id: getObjectId('eraser_line', true), type: 'eraser_line', points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], strokeWidth: toolState.eraser.width, @@ -256,7 +255,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getRectShapeId(selectedEntityAdapter.id, nanoid(), true), + id: getObjectId('rect_shape', true), type: 'rect_shape', x: pos.x - selectedEntity.position.x, y: pos.y - selectedEntity.position.y, @@ -356,7 +355,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getBrushLineId(selectedEntityAdapter.id, nanoid(), true), + id: getObjectId('brush_line', true), type: 'brush_line', points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], strokeWidth: toolState.brush.width, @@ -388,7 +387,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getEraserLineId(selectedEntityAdapter.id, nanoid(), true), + id: getObjectId('eraser_line', true), type: 'eraser_line', points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], strokeWidth: toolState.eraser.width, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts index a5d3cdde2e..2fbe23ccff 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts @@ -6,13 +6,13 @@ export const getRGId = (entityId: string) => `region_${entityId}`; export const getLayerId = (entityId: string) => `layer_${entityId}`; export const getBrushLineId = (entityId: string, lineId: string, isBuffer?: boolean) => - `${entityId}.${isBuffer ? 'buffer_' : ''}brush_line_${lineId}`; + `${isBuffer ? 'buffer_' : ''}brush_line_${lineId}`; export const getEraserLineId = (entityId: string, lineId: string, isBuffer?: boolean) => - `${entityId}.${isBuffer ? 'buffer_' : ''}eraser_line_${lineId}`; + `${isBuffer ? 'buffer_' : ''}eraser_line_${lineId}`; export const getRectShapeId = (entityId: string, rectId: string, isBuffer?: boolean) => - `${entityId}.${isBuffer ? 'buffer_' : ''}rect_${rectId}`; -export const getImageObjectId = (entityId: string, imageId: string) => `${entityId}.image_${imageId}`; -export const getObjectGroupId = (entityId: string, groupId: string) => `${entityId}.objectGroup_${groupId}`; + `${isBuffer ? 'buffer_' : ''}rect_${rectId}`; +export const getImageObjectId = (entityId: string, imageId: string) => `image_${imageId}`; +export const getObjectGroupId = (entityId: string, groupId: string) => `objectGroup_${groupId}`; export const getLayerBboxId = (entityId: string) => `${entityId}.bbox`; export const getCAId = (entityId: string) => `control_adapter_${entityId}`; export const getIPAId = (entityId: string) => `ip_adapter_${entityId}`; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 9c8740d2be..8273c0455e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,12 +1,12 @@ import { getImageDataTransparency } from 'common/util/arrayBuffer'; import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { GenerationMode, Rect, RgbaColor } from 'features/controlLayers/store/types'; +import type { GenerationMode, Rect, RenderableObject, 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'; import type { Vector2d } from 'konva/lib/types'; -import { customAlphabet, urlAlphabet } from 'nanoid'; +import { customAlphabet } from 'nanoid'; import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; @@ -575,4 +575,19 @@ export function loadImage(src: string, imageEl?: HTMLImageElement): Promise<HTML }); } -export const nanoid = customAlphabet(urlAlphabet, 10); +/** + * Generates a random alphanumeric string of length 10. Probably not secure at all. + */ +export const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 10); + +export function getPrefixedId(prefix: string): string { + return `${prefix}:${nanoid()}`; +} + +export function getObjectId(type: RenderableObject['type'], isBuffer?: boolean): string { + if (isBuffer) { + return getPrefixedId(`buffer_${type}`); + } else { + return getPrefixedId(type); + } +} diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index b302a00ba7..8cf74b8bed 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -207,7 +207,6 @@ export const { bboxSizeOptimized, // layers layerAdded, - layerAddedFromStagingArea, layerRecalled, layerDeleted, layerReset, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts index 0283f4097c..94596d246b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts @@ -162,7 +162,7 @@ export const controlAdaptersReducers = { ca.bboxNeedsUpdate = true; ca.isEnabled = true; if (imageDTO) { - const newImageObject = imageDTOToImageObject(id, objectId, imageDTO, { filters: ca.filters }); + const newImageObject = imageDTOToImageObject(imageDTO, { filters: ca.filters }); if (isEqual(newImageObject, ca.imageObject)) { return; } @@ -185,9 +185,7 @@ export const controlAdaptersReducers = { ca.bbox = null; ca.bboxNeedsUpdate = true; ca.isEnabled = true; - ca.processedImageObject = imageDTO - ? imageDTOToImageObject(id, objectId, imageDTO, { filters: ca.filters }) - : null; + ca.processedImageObject = imageDTO ? imageDTOToImageObject(imageDTO, { filters: ca.filters }) : null; }, prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }), }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts index b30af45ab5..f50edeefaa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts @@ -25,7 +25,7 @@ export const initialImageReducers = { if (!state.initialImage) { return; } - const newImageObject = imageDTOToImageObject('initial_image', 'initial_image_object', imageDTO); + const newImageObject = imageDTOToImageObject(imageDTO); if (isEqual(newImageObject, state.initialImage.imageObject)) { return; } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts index ce29909b7d..60c4c78d08 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts @@ -4,13 +4,7 @@ import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; -import type { - CanvasV2State, - CLIPVisionModelV2, - IPAdapterConfig, - IPAdapterEntity, - IPMethodV2, -} from './types'; +import type { CanvasV2State, CLIPVisionModelV2, IPAdapterConfig, IPAdapterEntity, IPMethodV2 } from './types'; import { imageDTOToImageObject } from './types'; export const selectIPA = (state: CanvasV2State, id: string) => state.ipAdapters.entities.find((ipa) => ipa.id === id); @@ -61,7 +55,7 @@ export const ipAdaptersReducers = { if (!ipa) { return; } - ipa.imageObject = imageDTO ? imageDTOToImageObject(id, objectId, imageDTO) : null; + ipa.imageObject = imageDTO ? imageDTOToImageObject(imageDTO) : null; }, prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }), }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 56e7fda305..a05849153a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -1,7 +1,8 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; -import { nanoid } from 'features/controlLayers/konva/util'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { IRect } from 'konva/lib/types'; +import { merge } from 'lodash-es'; import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; @@ -16,7 +17,6 @@ import type { PositionChangedArg, RectShape, ScaleChangedArg, - StagingAreaImage, } from './types'; import { imageDTOToImageObject, imageDTOToImageWithDims } from './types'; @@ -29,42 +29,23 @@ export const selectLayerOrThrow = (state: CanvasV2State, id: string) => { export const layersReducers = { layerAdded: { - reducer: (state, action: PayloadAction<{ id: string }>) => { + reducer: (state, action: PayloadAction<{ id: string; overrides?: Partial<LayerEntity> }>) => { const { id } = action.payload; - state.layers.entities.push({ + const layer: LayerEntity = { id, type: 'layer', isEnabled: true, objects: [], opacity: 1, position: { x: 0, y: 0 }, - }); + }; + merge(layer, action.payload.overrides); + state.layers.entities.push(layer); state.selectedEntityIdentifier = { type: 'layer', id }; state.layers.imageCache = null; }, - prepare: () => ({ payload: { id: nanoid() } }), - }, - layerAddedFromStagingArea: { - reducer: ( - state, - action: PayloadAction<{ id: string; objectId: string; stagingAreaImage: StagingAreaImage; position: Coordinate }> - ) => { - const { id, objectId, stagingAreaImage, position } = action.payload; - const { imageDTO, offsetX, offsetY } = stagingAreaImage; - const imageObject = imageDTOToImageObject(id, objectId, imageDTO); - state.layers.entities.push({ - id, - type: 'layer', - isEnabled: true, - objects: [imageObject], - opacity: 1, - position: { x: position.x + offsetX, y: position.y + offsetY }, - }); - state.selectedEntityIdentifier = { type: 'layer', id }; - state.layers.imageCache = null; - }, - prepare: (payload: { stagingAreaImage: StagingAreaImage; position: Coordinate }) => ({ - payload: { ...payload, id: nanoid(), objectId: nanoid() }, + prepare: (payload: { overrides?: Partial<LayerEntity> }) => ({ + payload: { ...payload, id: getPrefixedId('layer') }, }), }, layerRecalled: (state, action: PayloadAction<{ data: LayerEntity }>) => { @@ -227,27 +208,22 @@ export const layersReducers = { layer.position.y = Math.round(position.y); state.layers.imageCache = null; }, - layerImageAdded: { - reducer: ( - state, - action: PayloadAction<ImageObjectAddedArg & { objectId: string; pos?: { x: number; y: number } }> - ) => { - const { id, objectId, imageDTO, pos } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - const imageObject = imageDTOToImageObject(id, objectId, imageDTO); - if (pos) { - imageObject.x = pos.x; - imageObject.y = pos.y; - } - layer.objects.push(imageObject); - state.layers.imageCache = null; - }, - prepare: (payload: ImageObjectAddedArg & { pos?: { x: number; y: number } }) => ({ - payload: { ...payload, objectId: nanoid() }, - }), + layerImageAdded: ( + state, + action: PayloadAction<ImageObjectAddedArg & { objectId: string; pos?: { x: number; y: number } }> + ) => { + const { id, imageDTO, pos } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + const imageObject = imageDTOToImageObject(imageDTO); + if (pos) { + imageObject.x = pos.x; + imageObject.y = pos.y; + } + layer.objects.push(imageObject); + state.layers.imageCache = null; }, layerImageCacheChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO | null }>) => { const { imageDTO } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index c23708b995..901bda00e1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -2,7 +2,7 @@ import type { CanvasControlAdapter } from 'features/controlLayers/konva/CanvasCo import { CanvasInpaintMask } from 'features/controlLayers/konva/CanvasInpaintMask'; import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import { CanvasRegion } from 'features/controlLayers/konva/CanvasRegion'; -import { getImageObjectId } from 'features/controlLayers/konva/naming'; +import { getObjectId } from 'features/controlLayers/konva/util'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { AspectRatioState } from 'features/parameters/components/DocumentSize/types'; import type { @@ -777,15 +777,10 @@ export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO) height, }); -export const imageDTOToImageObject = ( - entityId: string, - objectId: string, - imageDTO: ImageDTO, - overrides?: Partial<ImageObject> -): ImageObject => { +export const imageDTOToImageObject = (imageDTO: ImageDTO, overrides?: Partial<ImageObject>): ImageObject => { const { width, height, image_name } = imageDTO; return { - id: getImageObjectId(entityId, objectId), + id: getObjectId('image'), type: 'image', x: 0, y: 0,