diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
index 6d53d3a84d..f802319a06 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
@@ -1,6 +1,6 @@
 import { enqueueRequested } from 'app/store/actions';
 import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
-import { getCanvasManager } from 'features/controlLayers/konva/CanvasManager';
+import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
 import { sessionStagingAreaReset, sessionStartedStaging } from 'features/controlLayers/store/canvasV2Slice';
 import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
 import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph';
@@ -17,6 +17,9 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
       const model = state.canvasV2.params.model;
       const { prepend } = action.payload;
 
+      const manager = $canvasManager.get();
+      assert(manager, 'No model found in state');
+
       let didStartStaging = false;
       if (!state.canvasV2.session.isStaging && state.canvasV2.session.isActive) {
         dispatch(sessionStartedStaging());
@@ -26,7 +29,6 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
       try {
         let g;
 
-        const manager = getCanvasManager();
         assert(model, 'No model found in state');
         const base = model.base;
 
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx
index 7386461c27..7a3da1aacb 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx
@@ -1,6 +1,7 @@
 /* eslint-disable i18next/no-literal-string */
 import { Button } from '@chakra-ui/react';
 import { Flex } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
 import { useAppSelector } from 'app/store/storeHooks';
 import { BrushWidth } from 'features/controlLayers/components/BrushWidth';
 import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
@@ -10,19 +11,22 @@ import { NewSessionButton } from 'features/controlLayers/components/NewSessionBu
 import { ResetCanvasButton } from 'features/controlLayers/components/ResetCanvasButton';
 import { ToolChooser } from 'features/controlLayers/components/ToolChooser';
 import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup';
-import { getCanvasManager } from 'features/controlLayers/konva/CanvasManager';
+import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
 import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
 import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu';
 import { memo, useCallback } from 'react';
 
 export const ControlLayersToolbar = memo(() => {
   const tool = useAppSelector((s) => s.canvasV2.tool.selected);
+  const canvasManager = useStore($canvasManager);
   const bbox = useCallback(() => {
-    const manager = getCanvasManager();
-    for (const l of manager.layers.values()) {
+    if (!canvasManager) {
+      return;
+    }
+    for (const l of canvasManager.layers.values()) {
       l.getBbox();
     }
-  }, []);
+  }, [canvasManager]);
   return (
     <Flex w="full" gap={2}>
       <Flex flex={1} justifyContent="center">
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx
index 31072b0fa1..d6d63e1d0c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx
@@ -2,7 +2,7 @@ import { Flex } from '@invoke-ai/ui-library';
 import { logger } from 'app/logging/logger';
 import { useAppStore } from 'app/store/storeHooks';
 import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay';
-import { CanvasManager, setCanvasManager } from 'features/controlLayers/konva/CanvasManager';
+import { $canvasManager, CanvasManager } from 'features/controlLayers/konva/CanvasManager';
 import Konva from 'konva';
 import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
 import { useDevicePixelRatio } from 'use-device-pixel-ratio';
@@ -28,7 +28,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
     }
 
     const manager = new CanvasManager(stage, container, store);
-    setCanvasManager(manager);
+    $canvasManager.set(manager);
     console.log(manager);
     const cleanup = manager.initialize();
     return cleanup;
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx
index 607e5acc3d..cf70f59ee9 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx
@@ -1,38 +1,58 @@
 import { Button, IconButton } from '@invoke-ai/ui-library';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { toolIsTransformingChanged } from 'features/controlLayers/store/canvasV2Slice';
-import { memo, useCallback } from 'react';
+import { useStore } from '@nanostores/react';
+import { useAppSelector } from 'app/store/storeHooks';
+import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
+import { memo, useCallback, useEffect, useState } from 'react';
 import { useHotkeys } from 'react-hotkeys-hook';
 import { useTranslation } from 'react-i18next';
 import { PiResizeBold } from 'react-icons/pi';
 
 export const TransformToolButton = memo(() => {
   const { t } = useTranslation();
-  const dispatch = useAppDispatch();
-  const isTransforming = useAppSelector((s) => s.canvasV2.tool.isTransforming);
+  const canvasManager = useStore($canvasManager);
+  const [isTransforming, setIsTransforming] = useState(false);
   const isDisabled = useAppSelector(
     (s) => s.canvasV2.selectedEntityIdentifier === null || s.canvasV2.session.isStaging
   );
 
+  useEffect(() => {
+    if (!canvasManager) {
+      return;
+    }
+    canvasManager.onTransform = setIsTransforming;
+    return () => {
+      canvasManager.onTransform = null;
+    };
+  }, [canvasManager]);
+
   const onTransform = useCallback(() => {
-    dispatch(toolIsTransformingChanged(true));
-  }, [dispatch]);
+    if (!canvasManager) {
+      return;
+    }
+    canvasManager.startTransform();
+  }, [canvasManager]);
 
   const onApplyTransformation = useCallback(() => {
-    false && dispatch(toolIsTransformingChanged(true));
-  }, [dispatch]);
+    if (!canvasManager) {
+      return;
+    }
+    canvasManager.applyTransform();
+  }, [canvasManager]);
 
   const onCancelTransformation = useCallback(() => {
-    dispatch(toolIsTransformingChanged(false));
-  }, [dispatch]);
+    if (!canvasManager) {
+      return;
+    }
+    canvasManager.cancelTransform();
+  }, [canvasManager]);
 
   useHotkeys(['ctrl+t', 'meta+t'], onTransform, { enabled: !isDisabled }, [isDisabled, onTransform]);
 
   if (isTransforming) {
     return (
       <>
-        <Button onClick={onApplyTransformation}>Apply</Button>
-        <Button onClick={onCancelTransformation}>Cancel</Button>
+        <Button onClick={onApplyTransformation}>{t('common.apply')}</Button>
+        <Button onClick={onCancelTransformation}>{t('common.cancel')}</Button>
       </>
     );
   }
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts
index 84c824022f..3de306581f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts
@@ -44,8 +44,8 @@ export class CanvasBrushLine {
     this.state = state;
   }
 
-  update(state: BrushLine, force?: boolean): boolean {
-    if (this.state !== state || force) {
+  async update(state: BrushLine, force?: boolean): Promise<boolean> {
+    if (force || this.state !== state) {
       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
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts
index f1d93fe9a9..32e7ccbd24 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts
@@ -44,8 +44,8 @@ export class CanvasEraserLine {
     this.state = state;
   }
 
-  update(state: EraserLine, force?: boolean): boolean {
-    if (this.state !== state || force) {
+  async update(state: EraserLine, force?: boolean): Promise<boolean> {
+    if (force || this.state !== state) {
       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
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts
index 66fa5ca47d..cea1624f27 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts
@@ -1,14 +1,23 @@
-import { deepClone } from 'common/util/deepClone';
+import { getStore } from 'app/store/nanostores/store';
 import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine';
 import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine';
 import { CanvasImage } from 'features/controlLayers/konva/CanvasImage';
-import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
+import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
 import { CanvasRect } from 'features/controlLayers/konva/CanvasRect';
-import { mapId } from 'features/controlLayers/konva/util';
-import type { BrushLine, EraserLine, LayerEntity, RectShape } from 'features/controlLayers/store/types';
-import { isDrawingTool } from 'features/controlLayers/store/types';
+import { konvaNodeToBlob, mapId, previewBlob } from 'features/controlLayers/konva/util';
+import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice';
+import type {
+  BrushLine,
+  CanvasV2State,
+  Coordinate,
+  EraserLine,
+  LayerEntity,
+  RectShape,
+} from 'features/controlLayers/store/types';
 import Konva from 'konva';
-import { debounce } from 'lodash-es';
+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 {
@@ -20,8 +29,6 @@ export class CanvasLayer {
   static OBJECT_GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_object-group`;
   static BBOX_NAME = `${CanvasLayer.NAME_PREFIX}_bbox`;
 
-  private static BBOX_PADDING_PX = 5;
-
   private drawingBuffer: BrushLine | EraserLine | RectShape | null;
   private state: LayerEntity;
 
@@ -41,8 +48,10 @@ export class CanvasLayer {
   offsetY: number;
   width: number;
   height: number;
-
-  getBbox = debounce(this._getBbox, 300);
+  log: Logger;
+  bboxNeedsUpdate: boolean;
+  isTransforming: boolean;
+  isFirstRender: boolean;
 
   constructor(state: LayerEntity, manager: CanvasManager) {
     this.id = state.id;
@@ -60,12 +69,12 @@ export class CanvasLayer {
       objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }),
       transformer: new Konva.Transformer({
         name: CanvasLayer.TRANSFORMER_NAME,
-        draggable: true,
+        draggable: false,
         // enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
         rotateEnabled: true,
         flipEnabled: true,
         listening: false,
-        padding: CanvasLayer.BBOX_PADDING_PX,
+        padding: this.manager.getScaledBboxPadding(),
         stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400
         keepRatio: false,
       }),
@@ -135,19 +144,30 @@ export class CanvasLayer {
       });
 
       this.konva.objectGroup.setAttrs({
-        x: this.konva.interactionRect.x(),
-        y: this.konva.interactionRect.y(),
+        x: this.konva.interactionRect.x() - this.offsetX * this.konva.interactionRect.scaleX(),
+        y: this.konva.interactionRect.y() - this.offsetY * this.konva.interactionRect.scaleY(),
         scaleX: this.konva.interactionRect.scaleX(),
         scaleY: this.konva.interactionRect.scaleY(),
         rotation: this.konva.interactionRect.rotation(),
       });
+      console.log('objectGroup', {
+        x: this.konva.objectGroup.x(),
+        y: this.konva.objectGroup.y(),
+        scaleX: this.konva.objectGroup.scaleX(),
+        scaleY: this.konva.objectGroup.scaleY(),
+        offsetX: this.offsetX,
+        offsetY: this.offsetY,
+        width: this.konva.objectGroup.width(),
+        height: this.konva.objectGroup.height(),
+        rotation: this.konva.objectGroup.rotation(),
+      });
     });
 
     this.konva.transformer.on('transformend', () => {
-      this.offsetX = this.konva.interactionRect.x() - this.state.position.x;
-      this.offsetY = this.konva.interactionRect.y() - this.state.position.y;
-      this.width = Math.round(this.konva.interactionRect.width() * this.konva.interactionRect.scaleX());
-      this.height = Math.round(this.konva.interactionRect.height() * this.konva.interactionRect.scaleY());
+      // this.offsetX = this.konva.interactionRect.x() - this.state.position.x;
+      // this.offsetY = this.konva.interactionRect.y() - this.state.position.y;
+      // this.width = Math.round(this.konva.interactionRect.width() * this.konva.interactionRect.scaleX());
+      // this.height = Math.round(this.konva.interactionRect.height() * this.konva.interactionRect.scaleY());
       // this.manager.stateApi.onPosChanged(
       //   {
       //     id: this.id,
@@ -166,26 +186,33 @@ 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() - CanvasLayer.BBOX_PADDING_PX / this.manager.stage.scaleX(),
-        y: this.konva.interactionRect.y() - CanvasLayer.BBOX_PADDING_PX / this.manager.stage.scaleX(),
+        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
       // stored as this.bbox)
       this.konva.objectGroup.setAttrs({
-        x: this.konva.interactionRect.x(),
-        y: this.konva.interactionRect.y(),
+        x: this.konva.interactionRect.x() - this.offsetX * this.konva.interactionRect.scaleX(),
+        y: this.konva.interactionRect.y() - this.offsetY * this.konva.interactionRect.scaleY(),
       });
     });
     this.konva.interactionRect.on('dragend', () => {
       this.logBbox('dragend bbox');
 
-      // Update internal state
-      // this.state.position = { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() };
+      if (this.isTransforming) {
+        // When the user cancels the transformation, we need to reset the layer, so we should not update the layer's
+        // positition while we are transforming - bail out early.
+        return;
+      }
+
       this.manager.stateApi.onPosChanged(
         {
           id: this.id,
-          position: { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() },
+          position: {
+            x: this.konva.interactionRect.x() - this.offsetX * this.konva.interactionRect.scaleX(),
+            y: this.konva.interactionRect.y() - this.offsetY * this.konva.interactionRect.scaleY(),
+          },
         },
         'layer'
       );
@@ -198,15 +225,16 @@ export class CanvasLayer {
     this.offsetY = 0;
     this.width = 0;
     this.height = 0;
+    this.bboxNeedsUpdate = true;
+    this.isTransforming = false;
+    this.isFirstRender = true;
+    this.log = this.manager.getLogger(`layer_${this.id}`);
 
     console.log(this);
   }
 
-  private static get DEFAULT_BBOX_RECT() {
-    return { x: 0, y: 0, width: 0, height: 0 };
-  }
-
   destroy(): void {
+    this.log.debug(`Layer ${this.id} - destroying`);
     this.konva.layer.destroy();
   }
 
@@ -214,99 +242,222 @@ export class CanvasLayer {
     return this.drawingBuffer;
   }
 
-  updatePosition() {
-    const scale = this.manager.stage.scaleX();
-    const onePixel = 1 / scale;
-    const bboxPadding = CanvasLayer.BBOX_PADDING_PX / scale;
-
-    this.konva.objectGroup.setAttrs({
-      x: this.state.position.x,
-      y: this.state.position.y,
-      offsetX: this.offsetX,
-      offsetY: this.offsetY,
-    });
-    this.konva.bbox.setAttrs({
-      x: this.state.position.x - bboxPadding,
-      y: this.state.position.y - bboxPadding,
-      width: this.width + bboxPadding * 2,
-      height: this.height + bboxPadding * 2,
-      strokeWidth: onePixel,
-    });
-    this.konva.interactionRect.setAttrs({
-      x: this.state.position.x,
-      y: this.state.position.y,
-      width: this.width,
-      height: this.height,
-    });
-  }
-
   async setDrawingBuffer(obj: BrushLine | EraserLine | RectShape | null) {
     if (obj) {
       this.drawingBuffer = obj;
-      await this.renderObject(this.drawingBuffer, true);
-      this.updateGroup(true);
+      await this._renderObject(this.drawingBuffer, true);
     } else {
       this.drawingBuffer = null;
     }
   }
 
-  finalizeDrawingBuffer() {
+  async finalizeDrawingBuffer() {
     if (!this.drawingBuffer) {
       return;
     }
-    if (this.drawingBuffer.type === 'brush_line') {
-      this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: this.drawingBuffer }, 'layer');
-    } else if (this.drawingBuffer.type === 'eraser_line') {
-      this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: this.drawingBuffer }, 'layer');
-    } else if (this.drawingBuffer.type === 'rect_shape') {
-      this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: this.drawingBuffer }, 'layer');
-    }
+    const drawingBuffer = this.drawingBuffer;
     this.setDrawingBuffer(null);
+
+    if (drawingBuffer.type === 'brush_line') {
+      this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: drawingBuffer }, 'layer');
+    } else if (drawingBuffer.type === 'eraser_line') {
+      this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: drawingBuffer }, 'layer');
+    } else if (drawingBuffer.type === 'rect_shape') {
+      this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: drawingBuffer }, 'layer');
+    }
   }
 
-  async render(state: LayerEntity) {
-    this.state = deepClone(state);
+  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));
 
-    let didDraw = false;
+    if (!this.isFirstRender && state === this.state) {
+      this.log.trace('State unchanged, skipping update');
+      return;
+    }
 
-    const objectIds = state.objects.map(mapId);
+    this.log.debug('Updating');
+    const { position, objects, opacity, isEnabled } = state;
+
+    if (this.isFirstRender || position !== this.state.position) {
+      await this.updatePosition({ position });
+    }
+    if (this.isFirstRender || objects !== this.state.objects) {
+      await this.updateObjects({ objects });
+    }
+    if (this.isFirstRender || opacity !== this.state.opacity) {
+      await this.updateOpacity({ opacity });
+    }
+    if (this.isFirstRender || isEnabled !== this.state.isEnabled) {
+      await this.updateVisibility({ isEnabled });
+    }
+    await this.updateInteraction({ toolState, isSelected });
+    this.state = state;
+  }
+
+  async updateVisibility(arg?: { isEnabled: boolean }) {
+    this.log.trace('Updating visibility');
+    const isEnabled = get(arg, 'isEnabled', this.state.isEnabled);
+    const hasObjects = this.objects.size > 0 || this.drawingBuffer !== null;
+    this.konva.layer.visible(isEnabled || hasObjects);
+  }
+
+  async updatePosition(arg?: { position: Coordinate }) {
+    this.log.trace('Updating position');
+    const position = get(arg, 'position', this.state.position);
+    const bboxPadding = this.manager.getScaledBboxPadding();
+
+    this.konva.objectGroup.setAttrs({
+      x: position.x,
+      y: position.y,
+    });
+    this.konva.bbox.setAttrs({
+      x: position.x + this.offsetX * this.konva.interactionRect.scaleX() - bboxPadding,
+      y: position.y + this.offsetY * this.konva.interactionRect.scaleY() - bboxPadding,
+    });
+    this.konva.interactionRect.setAttrs({
+      x: position.x + this.offsetX * this.konva.interactionRect.scaleX(),
+      y: position.y + this.offsetY * this.konva.interactionRect.scaleY(),
+    });
+  }
+
+  async updateObjects(arg?: { objects: LayerEntity['objects'] }) {
+    this.log.trace('Updating objects');
+
+    const objects = get(arg, 'objects', this.state.objects);
+
+    const objectIds = objects.map(mapId);
     // Destroy any objects that are no longer in state
     for (const object of this.objects.values()) {
       if (!objectIds.includes(object.id) && object.id !== this.drawingBuffer?.id) {
         this.objects.delete(object.id);
         object.destroy();
-        didDraw = true;
+        this.bboxNeedsUpdate = true;
       }
     }
 
-    for (const obj of state.objects) {
-      if (await this.renderObject(obj)) {
-        didDraw = true;
+    for (const obj of objects) {
+      if (await this._renderObject(obj)) {
+        this.bboxNeedsUpdate = true;
       }
     }
 
     if (this.drawingBuffer) {
-      if (await this.renderObject(this.drawingBuffer)) {
-        didDraw = true;
+      if (await this._renderObject(this.drawingBuffer)) {
+        this.bboxNeedsUpdate = true;
       }
     }
-
-    this.renderBbox();
-    this.updateGroup(didDraw);
   }
 
-  private async renderObject(obj: LayerEntity['objects'][number], force = false): Promise<boolean> {
+  async updateOpacity(arg?: { opacity: number }) {
+    this.log.trace('Updating opacity');
+
+    const opacity = get(arg, 'opacity', this.state.opacity);
+
+    this.konva.objectGroup.opacity(opacity);
+  }
+
+  async 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));
+
+    if (this.objects.size === 0) {
+      // The layer is totally empty, we can just disable the layer
+      this.konva.layer.listening(false);
+      return;
+    }
+
+    if (isSelected && !this.isTransforming && toolState.selected === 'move') {
+      // We are moving this layer, it must be listening
+      this.konva.layer.listening(true);
+
+      // The transformer is not needed
+      this.konva.transformer.listening(false);
+      this.konva.transformer.nodes([]);
+
+      // The bbox rect should be visible and interaction rect listening for dragging
+      this.konva.bbox.visible(true);
+      this.konva.interactionRect.listening(true);
+    } else if (isSelected && this.isTransforming) {
+      // 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 = toolState.selected !== 'view';
+      this.konva.layer.listening(listening);
+      this.konva.interactionRect.listening(listening);
+      this.konva.transformer.listening(listening);
+
+      // The transformer transforms the interaction rect, not the object group
+      this.konva.transformer.nodes([this.konva.interactionRect]);
+
+      // Hide the bbox rect, the transformer will has its own bbox
+      this.konva.bbox.visible(false);
+    } else {
+      // The layer is not selected, or we are using a tool that doesn't need the layer to be listening - disable interaction stuff
+      this.konva.layer.listening(false);
+
+      // The transformer, bbox and interaction rect should be inactive
+      this.konva.transformer.listening(false);
+      this.konva.transformer.nodes([]);
+      this.konva.bbox.visible(false);
+      this.konva.interactionRect.listening(false);
+    }
+  }
+
+  async updateBbox() {
+    this.log.trace('Updating bbox');
+
+    const onePixel = this.manager.getScaledPixel();
+    const bboxPadding = this.manager.getScaledBboxPadding();
+
+    this.konva.bbox.setAttrs({
+      x: this.state.position.x + this.offsetX * this.konva.interactionRect.scaleX() - bboxPadding,
+      y: this.state.position.y + this.offsetY * this.konva.interactionRect.scaleY() - bboxPadding,
+      width: this.width + bboxPadding * 2,
+      height: this.height + bboxPadding * 2,
+      strokeWidth: onePixel,
+    });
+    this.konva.interactionRect.setAttrs({
+      x: this.state.position.x + this.offsetX * this.konva.interactionRect.scaleX(),
+      y: this.state.position.y + this.offsetY * this.konva.interactionRect.scaleY(),
+      width: this.width,
+      height: this.height,
+    });
+  }
+
+  async syncStageScale() {
+    this.log.trace('Syncing scale to stage');
+
+    const onePixel = this.manager.getScaledPixel();
+    const bboxPadding = this.manager.getScaledBboxPadding();
+
+    this.konva.bbox.setAttrs({
+      x: this.konva.interactionRect.x() - bboxPadding,
+      y: this.konva.interactionRect.y() - bboxPadding,
+      width: this.konva.interactionRect.width() * this.konva.interactionRect.scaleX() + bboxPadding * 2,
+      height: this.konva.interactionRect.height() * this.konva.interactionRect.scaleY() + bboxPadding * 2,
+      strokeWidth: onePixel,
+    });
+    this.konva.transformer.forceUpdate();
+  }
+
+  async _renderObject(obj: LayerEntity['objects'][number], force = false): Promise<boolean> {
     if (obj.type === 'brush_line') {
       let brushLine = this.objects.get(obj.id);
       assert(brushLine instanceof CanvasBrushLine || brushLine === undefined);
 
       if (!brushLine) {
+        console.log('creating new brush line');
         brushLine = new CanvasBrushLine(obj);
         this.objects.set(brushLine.id, brushLine);
         this.konva.objectGroup.add(brushLine.konva.group);
         return true;
       } else {
-        if (brushLine.update(obj, force)) {
+        console.log('updating brush line');
+        if (await brushLine.update(obj, force)) {
           return true;
         }
       }
@@ -320,7 +471,7 @@ export class CanvasLayer {
         this.konva.objectGroup.add(eraserLine.konva.group);
         return true;
       } else {
-        if (eraserLine.update(obj, force)) {
+        if (await eraserLine.update(obj, force)) {
           return true;
         }
       }
@@ -358,109 +509,70 @@ export class CanvasLayer {
     return false;
   }
 
-  updateGroup(didDraw: boolean) {
-    if (!this.state.isEnabled) {
-      this.konva.layer.visible(false);
-      return;
-    }
+  async startTransform() {
+    this.isTransforming = true;
 
-    if (didDraw) {
-      if (this.objects.size > 0) {
-        this.getBbox();
-      } else {
-        this.offsetX = 0;
-        this.offsetY = 0;
-        this.width = 0;
-        this.height = 0;
-        this.renderBbox();
-      }
-    }
+    // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or
+    // interaction rect are listening, it will interrupt the stage's drag events. So we should disable listening
+    // when the view tool is selected
+    const listening = this.manager.stateApi.getToolState().selected !== 'view';
 
-    this.konva.layer.visible(true);
-    this.konva.objectGroup.opacity(this.state.opacity);
-    const isSelected = this.manager.stateApi.getIsSelected(this.id);
-    const toolState = this.manager.stateApi.getToolState();
+    this.konva.layer.listening(listening);
+    this.konva.interactionRect.listening(listening);
+    this.konva.transformer.listening(listening);
 
-    const isMoving = toolState.selected === 'move' && isSelected;
+    // The transformer transforms the interaction rect, not the object group
+    this.konva.transformer.nodes([this.konva.interactionRect]);
 
-    this.konva.layer.listening(toolState.isTransforming || isMoving);
-    this.konva.transformer.listening(toolState.isTransforming);
-    this.konva.bbox.visible(isMoving);
-    this.konva.interactionRect.listening(toolState.isTransforming || isMoving);
-
-    if (this.objects.size === 0) {
-      // If the layer is totally empty, reset the cache and bail out.
-      this.konva.transformer.nodes([]);
-      if (this.konva.objectGroup.isCached()) {
-        this.konva.objectGroup.clearCache();
-      }
-    } else if (isSelected && toolState.isTransforming) {
-      // When the layer is selected and being moved, we should always cache it.
-      // We should update the cache if we drew to the layer.
-      if (!this.konva.objectGroup.isCached() || didDraw) {
-        // this.konva.objectGroup.cache();
-      }
-      // Activate the transformer - it *must* be transforming the interactionRect, not the group!
-      this.konva.transformer.nodes([this.konva.interactionRect]);
-      this.konva.transformer.forceUpdate();
-      this.konva.transformer.visible(true);
-    } else if (toolState.selected === 'move') {
-      // When the layer is selected and being moved, we should always cache it.
-      // We should update the cache if we drew to the layer.
-      if (!this.konva.objectGroup.isCached() || didDraw) {
-        // this.konva.objectGroup.cache();
-      }
-      // Activate the transformer
-      this.konva.transformer.nodes([]);
-      this.konva.transformer.forceUpdate();
-      this.konva.transformer.visible(false);
-    } else if (isSelected) {
-      // If the layer is selected but not using the move tool, we don't want the layer to be listening.
-      // The transformer also does not need to be active.
-      this.konva.transformer.nodes([]);
-      if (isDrawingTool(toolState.selected)) {
-        // We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we
-        // should never be cached.
-        if (this.konva.objectGroup.isCached()) {
-          this.konva.objectGroup.clearCache();
-        }
-      } else {
-        // We are using a non-drawing tool (move, view, bbox), so we should cache the layer.
-        // We should update the cache if we drew to the layer.
-        if (!this.konva.objectGroup.isCached() || didDraw) {
-          // this.konva.objectGroup.cache();
-        }
-      }
-    } else if (!isSelected) {
-      // Unselected layers should not be listening
-      // The transformer also does not need to be active.
-      this.konva.transformer.nodes([]);
-      // Update the layer's cache if it's not already cached or we drew to it.
-      if (!this.konva.objectGroup.isCached() || didDraw) {
-        // this.konva.objectGroup.cache();
-      }
-    }
+    // Hide the bbox rect, the transformer will has its own bbox
+    this.konva.bbox.visible(false);
   }
 
-  renderBbox() {
-    const toolState = this.manager.stateApi.getToolState();
-    if (toolState.isTransforming) {
-      return;
-    }
-    const isSelected = this.manager.stateApi.getIsSelected(this.id);
-    const hasBbox = this.width !== 0 && this.height !== 0;
-    this.konva.bbox.visible(hasBbox && isSelected && toolState.selected === 'move');
-    this.konva.interactionRect.visible(hasBbox);
-    this.updatePosition();
+  async resetScale() {
+    this.konva.objectGroup.scaleX(1);
+    this.konva.objectGroup.scaleY(1);
+    this.konva.bbox.scaleX(1);
+    this.konva.bbox.scaleY(1);
+    this.konva.interactionRect.scaleX(1);
+    this.konva.interactionRect.scaleY(1);
   }
 
-  private _getBbox() {
+  async applyTransform() {
+    this.isTransforming = false;
+    const objectGroupClone = this.konva.objectGroup.clone();
+    const rect = {
+      x: this.konva.interactionRect.x(),
+      y: this.konva.interactionRect.y(),
+      width: this.konva.interactionRect.width() * this.konva.interactionRect.scaleX(),
+      height: this.konva.interactionRect.height() * this.konva.interactionRect.scaleY(),
+    };
+    const blob = await konvaNodeToBlob(objectGroupClone, rect);
+    previewBlob(blob, 'transformed layer');
+    const imageDTO = await uploadImage(blob, `${this.id}_transform.png`, 'other', true, true);
+    const { dispatch } = getStore();
+    dispatch(layerRasterized({ id: this.id, imageDTO, position: this.konva.interactionRect.position() }));
+    this.isTransforming = false;
+    this.resetScale();
+  }
+
+  async cancelTransform() {
+    this.isTransforming = false;
+    this.resetScale();
+    await this.updatePosition({ position: this.state.position });
+    await this.updateBbox();
+    await this.updateInteraction({
+      toolState: this.manager.stateApi.getToolState(),
+      isSelected: this.manager.stateApi.getIsSelected(this.id),
+    });
+  }
+
+  getBbox = debounce(() => {
     if (this.objects.size === 0) {
       this.offsetX = 0;
       this.offsetY = 0;
       this.width = 0;
       this.height = 0;
-      this.renderBbox();
+      this.updateBbox();
       return;
     }
 
@@ -482,18 +594,8 @@ export class CanvasLayer {
       this.offsetY = rect.y;
       this.width = rect.width;
       this.height = rect.height;
-      // if (rect.width === 0 || rect.height === 0) {
-      //   this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
-      // } else {
-      //   this.bbox = {
-      //     x: rect.x,
-      //     y: rect.y,
-      //     width: rect.width,
-      //     height: rect.height,
-      //   };
-      // }
       this.logBbox('new bbox from client rect');
-      this.renderBbox();
+      this.updateBbox();
       return;
     }
 
@@ -523,11 +625,11 @@ export class CanvasLayer {
           this.height = 0;
         }
         this.logBbox('new bbox from worker');
-        this.renderBbox();
+        this.updateBbox();
         clone.destroy();
       }
     );
-  }
+  }, CanvasManager.BBOX_DEBOUNCE_MS);
 
   logBbox(msg: string = 'bbox') {
     console.log(msg, {
@@ -539,4 +641,13 @@ export class CanvasLayer {
       height: this.height,
     });
   }
+
+  getLayerRect() {
+    return {
+      x: this.state.position.x + this.offsetX,
+      y: this.state.position.y + this.offsetY,
+      width: this.width,
+      height: this.height,
+    };
+  }
 }
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts
index a294f71506..fa1caf11b4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts
@@ -16,6 +16,7 @@ import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLaye
 import type { CanvasV2State, GenerationMode } from 'features/controlLayers/store/types';
 import type Konva from 'konva';
 import { atom } from 'nanostores';
+import type { Logger } from 'roarr';
 import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } from 'services/api/endpoints/images';
 import type { ImageCategory, ImageDTO } from 'services/api/types';
 import { assert } from 'tsafe';
@@ -32,9 +33,6 @@ import { CanvasStateApi } from './CanvasStateApi';
 import { CanvasTool } from './CanvasTool';
 import { setStageEventHandlers } from './events';
 
-const log = logger('canvas');
-const workerLog = logger('worker');
-
 // type Extents = {
 //   minX: number;
 //   minY: number;
@@ -63,17 +61,12 @@ type Util = {
   ) => Promise<ImageDTO>;
 };
 
-const $canvasManager = atom<CanvasManager | null>(null);
-export function getCanvasManager() {
-  const nodeManager = $canvasManager.get();
-  assert(nodeManager !== null, 'Node manager not initialized');
-  return nodeManager;
-}
-export function setCanvasManager(nodeManager: CanvasManager) {
-  $canvasManager.set(nodeManager);
-}
+export const $canvasManager = atom<CanvasManager | null>(null);
 
 export class CanvasManager {
+  private static BBOX_PADDING_PX = 5;
+  static BBOX_DEBOUNCE_MS = 300;
+
   stage: Konva.Stage;
   container: HTMLDivElement;
   controlAdapters: Map<string, CanvasControlAdapter>;
@@ -86,6 +79,11 @@ export class CanvasManager {
   preview: CanvasPreview;
   background: CanvasBackground;
 
+  log: Logger;
+  workerLog: Logger;
+
+  onTransform: ((isTransforming: boolean) => void) | null;
+
   private store: Store<RootState>;
   private isFirstRender: boolean;
   private prevState: CanvasV2State;
@@ -106,6 +104,9 @@ export class CanvasManager {
     this.prevState = this.stateApi.getState();
     this.isFirstRender = true;
 
+    this.log = logger('canvas');
+    this.workerLog = logger('worker');
+
     this.util = {
       getImageDTO,
       uploadImage,
@@ -138,9 +139,9 @@ export class CanvasManager {
       const { type, data } = event.data;
       if (type === 'log') {
         if (data.ctx) {
-          workerLog[data.level](data.ctx, data.message);
+          this.workerLog[data.level](data.ctx, data.message);
         } else {
-          workerLog[data.level](data.message);
+          this.workerLog[data.level](data.message);
         }
       } else if (type === 'extents') {
         const task = this.tasks.get(data.id);
@@ -151,11 +152,17 @@ export class CanvasManager {
       }
     };
     this.worker.onerror = (event) => {
-      log.error({ message: event.message }, 'Worker error');
+      this.log.error({ message: event.message }, 'Worker error');
     };
     this.worker.onmessageerror = () => {
-      log.error('Worker message error');
+      this.log.error('Worker message error');
     };
+    this.onTransform = null;
+  }
+
+  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) {
@@ -172,27 +179,6 @@ export class CanvasManager {
     await this.initialImage.render(this.stateApi.getInitialImageState());
   }
 
-  async renderLayers() {
-    const { entities } = this.stateApi.getLayersState();
-
-    for (const canvasLayer of this.layers.values()) {
-      if (!entities.find((l) => l.id === canvasLayer.id)) {
-        canvasLayer.destroy();
-        this.layers.delete(canvasLayer.id);
-      }
-    }
-
-    for (const entity of entities) {
-      let adapter = this.layers.get(entity.id);
-      if (!adapter) {
-        adapter = new CanvasLayer(entity, this);
-        this.layers.set(adapter.id, adapter);
-        this.stage.add(adapter.konva.layer);
-      }
-      await adapter.render(entity);
-    }
-  }
-
   async renderRegions() {
     const { entities } = this.stateApi.getRegionsState();
 
@@ -245,9 +231,9 @@ export class CanvasManager {
     }
   }
 
-  renderBboxes() {
+  syncStageScale() {
     for (const layer of this.layers.values()) {
-      layer.renderBbox();
+      layer.syncStageScale();
     }
   }
 
@@ -283,22 +269,84 @@ export class CanvasManager {
     this.background.render();
   }
 
+  getTransformingLayer() {
+    return Array.from(this.layers.values()).find((layer) => layer.isTransforming);
+  }
+
+  getIsTransforming() {
+    return Boolean(this.getTransformingLayer());
+  }
+
+  startTransform() {
+    if (this.getIsTransforming()) {
+      return;
+    }
+    const layer = this.getSelectedEntityAdapter();
+    assert(layer instanceof CanvasLayer, 'No selected layer');
+    layer.startTransform();
+    this.onTransform?.(true);
+  }
+
+  applyTransform() {
+    const layer = this.getTransformingLayer();
+    if (layer) {
+      layer.applyTransform();
+    }
+    this.onTransform?.(false);
+  }
+
+  cancelTransform() {
+    const layer = this.getTransformingLayer();
+    if (layer) {
+      layer.cancelTransform();
+    }
+    this.onTransform?.(false);
+  }
+
   render = async () => {
     const state = this.stateApi.getState();
 
     if (this.prevState === state && !this.isFirstRender) {
-      log.trace('No changes detected, skipping render');
+      this.log.trace('No changes detected, skipping render');
       return;
     }
 
+    if (this.isFirstRender || state.layers.entities !== this.prevState.layers.entities) {
+      this.log.debug('Rendering layers');
+
+      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}`);
+          canvasLayer.destroy();
+          this.layers.delete(canvasLayer.id);
+        }
+      }
+
+      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);
+        }
+        await adapter.update({
+          state: entityState,
+          toolState: state.tool,
+          isSelected: state.selectedEntityIdentifier?.id === entityState.id,
+        });
+      }
+    }
+
     if (
       this.isFirstRender ||
-      state.layers.entities !== this.prevState.layers.entities ||
       state.tool.selected !== this.prevState.tool.selected ||
       state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
     ) {
-      log.debug('Rendering layers');
-      await this.renderLayers();
+      this.log.debug('Updating interaction');
+      for (const layer of this.layers.values()) {
+        layer.updateInteraction({ toolState: state.tool, isSelected: state.selectedEntityIdentifier?.id === layer.id });
+      }
     }
 
     if (
@@ -308,7 +356,7 @@ export class CanvasManager {
       state.tool.selected !== this.prevState.tool.selected ||
       state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
     ) {
-      log.debug('Rendering initial image');
+      this.log.debug('Rendering initial image');
       await this.renderInitialImage();
     }
 
@@ -319,7 +367,7 @@ export class CanvasManager {
       state.tool.selected !== this.prevState.tool.selected ||
       state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
     ) {
-      log.debug('Rendering regions');
+      this.log.debug('Rendering regions');
       await this.renderRegions();
     }
 
@@ -330,7 +378,7 @@ export class CanvasManager {
       state.tool.selected !== this.prevState.tool.selected ||
       state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
     ) {
-      log.debug('Rendering inpaint mask');
+      this.log.debug('Rendering inpaint mask');
       await this.renderInpaintMask();
     }
 
@@ -340,7 +388,7 @@ export class CanvasManager {
       state.tool.selected !== this.prevState.tool.selected ||
       state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
     ) {
-      log.debug('Rendering control adapters');
+      this.log.debug('Rendering control adapters');
       await this.renderControlAdapters();
     }
 
@@ -350,7 +398,7 @@ export class CanvasManager {
       state.tool.selected !== this.prevState.tool.selected ||
       state.session.isActive !== this.prevState.session.isActive
     ) {
-      log.debug('Rendering generation bbox');
+      this.log.debug('Rendering generation bbox');
       await this.preview.bbox.render();
     }
 
@@ -360,12 +408,12 @@ export class CanvasManager {
       state.controlAdapters !== this.prevState.controlAdapters ||
       state.regions !== this.prevState.regions
     ) {
-      // log.debug('Updating entity bboxes');
+      // this.log.debug('Updating entity bboxes');
       // debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged);
     }
 
     if (this.isFirstRender || state.session !== this.prevState.session) {
-      log.debug('Rendering staging area');
+      this.log.debug('Rendering staging area');
       await this.preview.stagingArea.render();
     }
 
@@ -377,7 +425,7 @@ export class CanvasManager {
       state.inpaintMask !== this.prevState.inpaintMask ||
       state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
     ) {
-      log.debug('Arranging entities');
+      this.log.debug('Arranging entities');
       await this.arrangeEntities();
     }
 
@@ -389,7 +437,7 @@ export class CanvasManager {
   };
 
   initialize = () => {
-    log.debug('Initializing renderer');
+    this.log.debug('Initializing renderer');
     this.stage.container(this.container);
 
     const cleanupListeners = setStageEventHandlers(this);
@@ -405,24 +453,24 @@ export class CanvasManager {
     // When we this flag, we need to render the staging area
     $shouldShowStagedImage.subscribe(async (shouldShowStagedImage, prevShouldShowStagedImage) => {
       if (shouldShowStagedImage !== prevShouldShowStagedImage) {
-        log.debug('Rendering staging area');
+        this.log.debug('Rendering staging area');
         await this.preview.stagingArea.render();
       }
     });
 
     $lastProgressEvent.subscribe(async (lastProgressEvent, prevLastProgressEvent) => {
       if (lastProgressEvent !== prevLastProgressEvent) {
-        log.debug('Rendering progress image');
+        this.log.debug('Rendering progress image');
         await this.preview.progressPreview.render(lastProgressEvent);
       }
     });
 
-    log.debug('First render of konva stage');
+    this.log.debug('First render of konva stage');
     this.preview.tool.render();
     this.render();
 
     return () => {
-      log.debug('Cleaning up konva renderer');
+      this.log.debug('Cleaning up konva renderer');
       unsubscribeRenderer();
       cleanupListeners();
       $shouldShowStagedImage.off();
@@ -430,6 +478,19 @@ export class CanvasManager {
     };
   };
 
+  getStageScale(): number {
+    // The stage is never scaled differently in x and y
+    return this.stage.scaleX();
+  }
+
+  getScaledPixel(): number {
+    return 1 / this.getStageScale();
+  }
+
+  getScaledBboxPadding(): number {
+    return CanvasManager.BBOX_PADDING_PX / this.getStageScale();
+  }
+
   getSelectedEntityAdapter = (): CanvasLayer | CanvasRegion | CanvasControlAdapter | CanvasInpaintMask | null => {
     const state = this.stateApi.getState();
     const identifier = state.selectedEntityIdentifier;
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts
index d3a94b13ce..6b54330f6a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts
@@ -185,7 +185,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
         if (e.evt.shiftKey && lastLinePoint) {
           // Create a straight line from the last line point
           if (selectedEntityAdapter.getDrawingBuffer()) {
-            selectedEntityAdapter.finalizeDrawingBuffer();
+            await selectedEntityAdapter.finalizeDrawingBuffer();
           }
           await selectedEntityAdapter.setDrawingBuffer({
             id: getBrushLineId(selectedEntityAdapter.id, uuidv4()),
@@ -203,7 +203,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
           });
         } else {
           if (selectedEntityAdapter.getDrawingBuffer()) {
-            selectedEntityAdapter.finalizeDrawingBuffer();
+            await selectedEntityAdapter.finalizeDrawingBuffer();
           }
           await selectedEntityAdapter.setDrawingBuffer({
             id: getBrushLineId(selectedEntityAdapter.id, uuidv4()),
@@ -222,7 +222,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
         if (e.evt.shiftKey && lastLinePoint) {
           // Create a straight line from the last line point
           if (selectedEntityAdapter.getDrawingBuffer()) {
-            selectedEntityAdapter.finalizeDrawingBuffer();
+            await selectedEntityAdapter.finalizeDrawingBuffer();
           }
           await selectedEntityAdapter.setDrawingBuffer({
             id: getBrushLineId(selectedEntityAdapter.id, uuidv4()),
@@ -239,7 +239,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
           });
         } else {
           if (selectedEntityAdapter.getDrawingBuffer()) {
-            selectedEntityAdapter.finalizeDrawingBuffer();
+            await selectedEntityAdapter.finalizeDrawingBuffer();
           }
           await selectedEntityAdapter.setDrawingBuffer({
             id: getEraserLineId(selectedEntityAdapter.id, uuidv4()),
@@ -254,7 +254,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
 
       if (toolState.selected === 'rect') {
         if (selectedEntityAdapter.getDrawingBuffer()) {
-          selectedEntityAdapter.finalizeDrawingBuffer();
+          await selectedEntityAdapter.finalizeDrawingBuffer();
         }
         await selectedEntityAdapter.setDrawingBuffer({
           id: getRectShapeId(selectedEntityAdapter.id, uuidv4()),
@@ -290,7 +290,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
       if (toolState.selected === 'brush') {
         const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
         if (drawingBuffer?.type === 'brush_line') {
-          selectedEntityAdapter.finalizeDrawingBuffer();
+          await selectedEntityAdapter.finalizeDrawingBuffer();
         } else {
           await selectedEntityAdapter.setDrawingBuffer(null);
         }
@@ -299,7 +299,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
       if (toolState.selected === 'eraser') {
         const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
         if (drawingBuffer?.type === 'eraser_line') {
-          selectedEntityAdapter.finalizeDrawingBuffer();
+          await selectedEntityAdapter.finalizeDrawingBuffer();
         } else {
           await selectedEntityAdapter.setDrawingBuffer(null);
         }
@@ -308,7 +308,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
       if (toolState.selected === 'rect') {
         const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
         if (drawingBuffer?.type === 'rect_shape') {
-          selectedEntityAdapter.finalizeDrawingBuffer();
+          await selectedEntityAdapter.finalizeDrawingBuffer();
         } else {
           await selectedEntityAdapter.setDrawingBuffer(null);
         }
@@ -354,7 +354,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
           }
         } else {
           if (selectedEntityAdapter.getDrawingBuffer()) {
-            selectedEntityAdapter.finalizeDrawingBuffer();
+            await selectedEntityAdapter.finalizeDrawingBuffer();
           }
           await selectedEntityAdapter.setDrawingBuffer({
             id: getBrushLineId(selectedEntityAdapter.id, uuidv4()),
@@ -386,7 +386,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
           }
         } else {
           if (selectedEntityAdapter.getDrawingBuffer()) {
-            selectedEntityAdapter.finalizeDrawingBuffer();
+            await selectedEntityAdapter.finalizeDrawingBuffer();
           }
           await selectedEntityAdapter.setDrawingBuffer({
             id: getEraserLineId(selectedEntityAdapter.id, uuidv4()),
@@ -437,16 +437,16 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
       if (toolState.selected === 'brush' && drawingBuffer?.type === 'brush_line') {
         drawingBuffer.points.push(pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y);
         await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
-        selectedEntityAdapter.finalizeDrawingBuffer();
+        await selectedEntityAdapter.finalizeDrawingBuffer();
       } else if (toolState.selected === 'eraser' && drawingBuffer?.type === 'eraser_line') {
         drawingBuffer.points.push(pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y);
         await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
-        selectedEntityAdapter.finalizeDrawingBuffer();
+        await selectedEntityAdapter.finalizeDrawingBuffer();
       } else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect_shape') {
         drawingBuffer.width = pos.x - selectedEntity.position.x - drawingBuffer.x;
         drawingBuffer.height = pos.y - selectedEntity.position.y - drawingBuffer.y;
         await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
-        selectedEntityAdapter.finalizeDrawingBuffer();
+        await selectedEntityAdapter.finalizeDrawingBuffer();
       }
     }
 
@@ -496,7 +496,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
           scale: newScale,
         });
         manager.background.render();
-        manager.renderBboxes();
+        manager.syncStageScale();
       }
     }
     manager.preview.tool.render();
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts
index 15b816f8e7..b302a00ba7 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts
@@ -66,6 +66,7 @@ const initialState: CanvasV2State = {
     eraser: {
       width: 50,
     },
+    isTransforming: false,
   },
   bbox: {
     rect: { x: 0, y: 0, width: 512, height: 512 },
@@ -194,7 +195,6 @@ export const {
   allEntitiesDeleted,
   clipToBboxChanged,
   canvasReset,
-  toolIsTransformingChanged,
   // bbox
   bboxChanged,
   bboxScaledSizeChanged,
@@ -226,6 +226,7 @@ export const {
   layerBrushLineAdded,
   layerEraserLineAdded,
   layerRectShapeAdded,
+  layerRasterized,
   // IP Adapters
   ipaAdded,
   ipaRecalled,
@@ -396,3 +397,6 @@ export const sessionRequested = createAction(`${canvasV2Slice.name}/sessionReque
 export const sessionStagingAreaImageAccepted = createAction<{ index: number }>(
   `${canvasV2Slice.name}/sessionStagingAreaImageAccepted`
 );
+export const transformationApplied = createAction<CanvasEntityIdentifier>(
+  `${canvasV2Slice.name}/transformationApplied`
+);
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts
index f0cf91e1d0..5d538bd821 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts
@@ -34,8 +34,6 @@ export const layersReducers = {
         id,
         type: 'layer',
         isEnabled: true,
-        bbox: null,
-        bboxNeedsUpdate: false,
         objects: [],
         opacity: 1,
         position: { x: 0, y: 0 },
@@ -57,8 +55,6 @@ export const layersReducers = {
         id,
         type: 'layer',
         isEnabled: true,
-        bbox: null,
-        bboxNeedsUpdate: true,
         objects: [imageObject],
         opacity: 1,
         position: { x: position.x + offsetX, y: position.y + offsetY },
@@ -100,8 +96,6 @@ export const layersReducers = {
     if (!layer) {
       return;
     }
-    layer.bbox = bbox;
-    layer.bboxNeedsUpdate = false;
     if (bbox === null) {
       // TODO(psyche): Clear objects when bbox is cleared - right now this doesn't work bc bbox calculation for layers
       // doesn't work - always returns null
@@ -116,8 +110,6 @@ export const layersReducers = {
     }
     layer.isEnabled = true;
     layer.objects = [];
-    layer.bbox = null;
-    layer.bboxNeedsUpdate = false;
     state.layers.imageCache = null;
     layer.position = { x: 0, y: 0 };
   },
@@ -183,7 +175,6 @@ export const layersReducers = {
     }
 
     layer.objects.push(brushLine);
-    layer.bboxNeedsUpdate = true;
     state.layers.imageCache = null;
   },
   layerEraserLineAdded: (state, action: PayloadAction<{ id: string; eraserLine: EraserLine }>) => {
@@ -194,7 +185,6 @@ export const layersReducers = {
     }
 
     layer.objects.push(eraserLine);
-    layer.bboxNeedsUpdate = true;
     state.layers.imageCache = null;
   },
   layerRectShapeAdded: (state, action: PayloadAction<{ id: string; rectShape: RectShape }>) => {
@@ -205,7 +195,6 @@ export const layersReducers = {
     }
 
     layer.objects.push(rectShape);
-    layer.bboxNeedsUpdate = true;
     state.layers.imageCache = null;
   },
   layerScaled: (state, action: PayloadAction<ScaleChangedArg>) => {
@@ -235,7 +224,6 @@ export const layersReducers = {
     }
     layer.position.x = Math.round(position.x);
     layer.position.y = Math.round(position.y);
-    layer.bboxNeedsUpdate = true;
     state.layers.imageCache = null;
   },
   layerImageAdded: {
@@ -254,7 +242,6 @@ export const layersReducers = {
         imageObject.y = pos.y;
       }
       layer.objects.push(imageObject);
-      layer.bboxNeedsUpdate = true;
       state.layers.imageCache = null;
     },
     prepare: (payload: ImageObjectAddedArg & { pos?: { x: number; y: number } }) => ({
@@ -265,6 +252,16 @@ export const layersReducers = {
     const { imageDTO } = action.payload;
     state.layers.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
   },
+  layerRasterized: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO; position: Coordinate }>) => {
+    const { id, imageDTO, position } = action.payload;
+    const layer = selectLayer(state, id);
+    if (!layer) {
+      return;
+    }
+    layer.objects = [imageDTOToImageObject(id, uuidv4(), imageDTO)];
+    layer.position = position;
+    state.layers.imageCache = null;
+  },
 } satisfies SliceCaseReducers<CanvasV2State>;
 
 const scalePoints = (points: number[], scaleX: number, scaleY: number) => {
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts
index 3724f4942b..c1f14d7df4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts
@@ -20,7 +20,4 @@ export const toolReducers = {
   toolBufferChanged: (state, action: PayloadAction<Tool | null>) => {
     state.tool.selectedBuffer = action.payload;
   },
-  toolIsTransformingChanged: (state, action: PayloadAction<boolean>) => {
-    state.tool.isTransforming = action.payload;
-  },
 } satisfies SliceCaseReducers<CanvasV2State>;
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index a6aa5936f8..c23708b995 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -579,8 +579,6 @@ export const zLayerEntity = z.object({
   type: z.literal('layer'),
   isEnabled: z.boolean(),
   position: zCoordinate,
-  bbox: zRect.nullable(),
-  bboxNeedsUpdate: z.boolean(),
   opacity: zOpacity,
   objects: z.array(zRenderableObject),
 });
@@ -850,7 +848,6 @@ export type CanvasV2State = {
     brush: { width: number };
     eraser: { width: number };
     fill: RgbaColor;
-    isTransforming: boolean;
   };
   settings: {
     imageSmoothing: boolean;
diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts
index 43b6347b5f..9770cfd3de 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/images.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts
@@ -590,11 +590,14 @@ export const uploadImage = async (
   blob: Blob,
   fileName: string,
   image_category: ImageCategory,
-  is_intermediate: boolean
+  is_intermediate: boolean,
+  crop_visible: boolean = false
 ): Promise<ImageDTO> => {
   const { dispatch } = getStore();
   const file = new File([blob], fileName, { type: 'image/png' });
-  const req = dispatch(imagesApi.endpoints.uploadImage.initiate({ file, image_category, is_intermediate }));
+  const req = dispatch(
+    imagesApi.endpoints.uploadImage.initiate({ file, image_category, is_intermediate, crop_visible })
+  );
   req.reset();
   return await req.unwrap();
 };