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,