diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BboxToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BboxToolButton.tsx
index 70440f10ac..6eecd06860 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/BboxToolButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/BboxToolButton.tsx
@@ -9,21 +9,22 @@ import { PiBoundingBoxBold } from 'react-icons/pi';
export const BboxToolButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
- const isDisabled = useAppSelector((s) => s.canvasV2.session.isStaging);
+ const isDisabled = useAppSelector((s) => s.canvasV2.session.isStaging || s.canvasV2.tool.isTransforming);
const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'bbox');
const onClick = useCallback(() => {
dispatch(toolChanged('bbox'));
}, [dispatch]);
- useHotkeys('q', onClick, [onClick]);
+ useHotkeys('q', onClick, { enabled: !isDisabled }, [onClick, isDisabled]);
return (
}
- variant={isSelected ? 'solid' : 'outline'}
+ colorScheme={isSelected ? 'invokeBlue' : 'base'}
+ variant="outline"
onClick={onClick}
isDisabled={isDisabled}
/>
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BrushToolButton.tsx
index 0dcaa7fa7c..551568e7cf 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/BrushToolButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/BrushToolButton.tsx
@@ -15,13 +15,13 @@ export const BrushToolButton = memo(() => {
const entityType = s.canvasV2.selectedEntityIdentifier?.type;
const isDrawingToolAllowed = entityType ? isDrawableEntityType(entityType) : false;
const isStaging = s.canvasV2.session.isStaging;
- return !isDrawingToolAllowed || isStaging;
+ return !isDrawingToolAllowed || isStaging || s.canvasV2.tool.isTransforming;
});
const onClick = useCallback(() => {
dispatch(toolChanged('brush'));
}, [dispatch]);
-
+
useHotkeys('b', onClick, { enabled: !isDisabled }, [isDisabled, onClick]);
return (
@@ -29,7 +29,8 @@ export const BrushToolButton = memo(() => {
aria-label={`${t('unifiedCanvas.brush')} (B)`}
tooltip={`${t('unifiedCanvas.brush')} (B)`}
icon={}
- variant={isSelected ? 'solid' : 'outline'}
+ colorScheme={isSelected ? 'invokeBlue' : 'base'}
+ variant="outline"
onClick={onClick}
isDisabled={isDisabled}
/>
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/EraserToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/EraserToolButton.tsx
index 698b37c81f..f73d38a769 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/EraserToolButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/EraserToolButton.tsx
@@ -15,7 +15,7 @@ export const EraserToolButton = memo(() => {
const entityType = s.canvasV2.selectedEntityIdentifier?.type;
const isDrawingToolAllowed = entityType ? isDrawableEntityType(entityType) : false;
const isStaging = s.canvasV2.session.isStaging;
- return !isDrawingToolAllowed || isStaging;
+ return !isDrawingToolAllowed || isStaging || s.canvasV2.tool.isTransforming;
});
const onClick = useCallback(() => {
@@ -29,7 +29,8 @@ export const EraserToolButton = memo(() => {
aria-label={`${t('unifiedCanvas.eraser')} (E)`}
tooltip={`${t('unifiedCanvas.eraser')} (E)`}
icon={}
- variant={isSelected ? 'solid' : 'outline'}
+ colorScheme={isSelected ? 'invokeBlue' : 'base'}
+ variant="outline"
onClick={onClick}
isDisabled={isDisabled}
/>
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/MoveToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/MoveToolButton.tsx
index 48dcfeb247..5d97542369 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/MoveToolButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/MoveToolButton.tsx
@@ -11,7 +11,7 @@ export const MoveToolButton = memo(() => {
const dispatch = useAppDispatch();
const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'move');
const isDisabled = useAppSelector(
- (s) => s.canvasV2.selectedEntityIdentifier === null || s.canvasV2.session.isStaging
+ (s) => s.canvasV2.selectedEntityIdentifier === null || s.canvasV2.session.isStaging || s.canvasV2.tool.isTransforming
);
const onClick = useCallback(() => {
@@ -25,7 +25,8 @@ export const MoveToolButton = memo(() => {
aria-label={`${t('unifiedCanvas.move')} (V)`}
tooltip={`${t('unifiedCanvas.move')} (V)`}
icon={}
- variant={isSelected ? 'solid' : 'outline'}
+ colorScheme={isSelected ? 'invokeBlue' : 'base'}
+ variant="outline"
onClick={onClick}
isDisabled={isDisabled}
/>
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RectToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RectToolButton.tsx
index 4a8ccadd09..3c8acd4ae8 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RectToolButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RectToolButton.tsx
@@ -15,7 +15,7 @@ export const RectToolButton = memo(() => {
const entityType = s.canvasV2.selectedEntityIdentifier?.type;
const isDrawingToolAllowed = entityType ? isDrawableEntityType(entityType) : false;
const isStaging = s.canvasV2.session.isStaging;
- return !isDrawingToolAllowed || isStaging;
+ return !isDrawingToolAllowed || isStaging || s.canvasV2.tool.isTransforming;
});
const onClick = useCallback(() => {
@@ -29,7 +29,8 @@ export const RectToolButton = memo(() => {
aria-label={`${t('controlLayers.rectangle')} (U)`}
tooltip={`${t('controlLayers.rectangle')} (U)`}
icon={}
- variant={isSelected ? 'solid' : 'outline'}
+ colorScheme={isSelected ? 'invokeBlue' : 'base'}
+ variant="outline"
onClick={onClick}
isDisabled={isDisabled}
/>
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx
index b9b6c8ca84..6d44039127 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx
@@ -14,23 +14,26 @@ export const ToolChooser: React.FC = () => {
useCanvasResetLayerHotkey();
useCanvasDeleteLayerHotkey();
const isCanvasSessionActive = useAppSelector((s) => s.canvasV2.session.isActive);
+ const isTransforming = useAppSelector((s) => s.canvasV2.tool.isTransforming);
if (isCanvasSessionActive) {
return (
-
-
-
-
-
+ <>
+
+
+
+
+
+
+
+
-
-
-
+ >
);
}
return (
-
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx
index e8ac2e2577..607e5acc3d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx
@@ -1,6 +1,6 @@
-import { IconButton } from '@invoke-ai/ui-library';
+import { Button, IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { toolChanged } from 'features/controlLayers/store/canvasV2Slice';
+import { toolIsTransformingChanged } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
@@ -9,24 +9,41 @@ import { PiResizeBold } from 'react-icons/pi';
export const TransformToolButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
- const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'transform');
+ const isTransforming = useAppSelector((s) => s.canvasV2.tool.isTransforming);
const isDisabled = useAppSelector(
(s) => s.canvasV2.selectedEntityIdentifier === null || s.canvasV2.session.isStaging
);
- const onClick = useCallback(() => {
- dispatch(toolChanged('transform'));
+ const onTransform = useCallback(() => {
+ dispatch(toolIsTransformingChanged(true));
}, [dispatch]);
- useHotkeys(['ctrl+t', 'meta+t'], onClick, { enabled: !isDisabled }, [isDisabled, onClick]);
+ const onApplyTransformation = useCallback(() => {
+ false && dispatch(toolIsTransformingChanged(true));
+ }, [dispatch]);
+
+ const onCancelTransformation = useCallback(() => {
+ dispatch(toolIsTransformingChanged(false));
+ }, [dispatch]);
+
+ useHotkeys(['ctrl+t', 'meta+t'], onTransform, { enabled: !isDisabled }, [isDisabled, onTransform]);
+
+ if (isTransforming) {
+ return (
+ <>
+
+
+ >
+ );
+ }
return (
}
- variant={isSelected ? 'solid' : 'outline'}
- onClick={onClick}
+ variant="solid"
+ onClick={onTransform}
isDisabled={isDisabled}
/>
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ViewToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ViewToolButton.tsx
index b9f6b1691d..184e38d7ad 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/ViewToolButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ViewToolButton.tsx
@@ -10,19 +10,20 @@ export const ViewToolButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'view');
- const isDisabled = useAppSelector((s) => s.canvasV2.session.isStaging);
+ const isDisabled = useAppSelector((s) => s.canvasV2.session.isStaging || s.canvasV2.tool.isTransforming);
const onClick = useCallback(() => {
dispatch(toolChanged('view'));
}, [dispatch]);
- useHotkeys('h', onClick, [onClick]);
+ useHotkeys('h', onClick, { enabled: !isDisabled }, [onClick]);
return (
}
- variant={isSelected ? 'solid' : 'outline'}
+ colorScheme={isSelected ? 'invokeBlue' : 'base'}
+ variant="outline"
onClick={onClick}
isDisabled={isDisabled}
/>
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts
index 0ae6ca3c62..66fa5ca47d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts
@@ -5,14 +5,12 @@ import { CanvasImage } from 'features/controlLayers/konva/CanvasImage';
import type { 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, Rect, RectShape } from 'features/controlLayers/store/types';
+import type { BrushLine, EraserLine, LayerEntity, RectShape } from 'features/controlLayers/store/types';
import { isDrawingTool } from 'features/controlLayers/store/types';
import Konva from 'konva';
import { debounce } from 'lodash-es';
import { assert } from 'tsafe';
-const MIN_LAYER_SIZE_PX = 10;
-
export class CanvasLayer {
static NAME_PREFIX = 'layer';
static LAYER_NAME = `${CanvasLayer.NAME_PREFIX}_layer`;
@@ -34,14 +32,15 @@ export class CanvasLayer {
layer: Konva.Layer;
bbox: Konva.Rect;
objectGroup: Konva.Group;
- objectGroupBbox: Konva.Rect;
- positionXLine: Konva.Line;
- positionYLine: Konva.Line;
transformer: Konva.Transformer;
interactionRect: Konva.Rect;
};
objects: Map;
- bbox: Rect;
+
+ offsetX: number;
+ offsetY: number;
+ width: number;
+ height: number;
getBbox = debounce(this._getBbox, 300);
@@ -59,18 +58,16 @@ export class CanvasLayer {
strokeHitEnabled: false,
}),
objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }),
- objectGroupBbox: new Konva.Rect({ fill: 'green', opacity: 0.5, listening: false }),
- positionXLine: new Konva.Line({ stroke: 'white', strokeWidth: 1 }),
- positionYLine: new Konva.Line({ stroke: 'white', strokeWidth: 1 }),
transformer: new Konva.Transformer({
name: CanvasLayer.TRANSFORMER_NAME,
- draggable: false,
+ draggable: true,
// enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
- rotateEnabled: false,
- flipEnabled: false,
+ rotateEnabled: true,
+ flipEnabled: true,
listening: false,
padding: CanvasLayer.BBOX_PADDING_PX,
stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400
+ keepRatio: false,
}),
interactionRect: new Konva.Rect({
name: CanvasLayer.INTERACTION_RECT_NAME,
@@ -84,9 +81,6 @@ export class CanvasLayer {
this.konva.layer.add(this.konva.transformer);
this.konva.layer.add(this.konva.interactionRect);
this.konva.layer.add(this.konva.bbox);
- this.konva.layer.add(this.konva.objectGroupBbox);
- this.konva.layer.add(this.konva.positionXLine);
- this.konva.layer.add(this.konva.positionYLine);
this.konva.transformer.on('transformstart', () => {
console.log('>>> transformstart');
@@ -98,35 +92,35 @@ export class CanvasLayer {
width: this.konva.interactionRect.width(),
height: this.konva.interactionRect.height(),
});
- console.log('this.bbox', deepClone(this.bbox));
+ this.logBbox('transformstart bbox');
console.log('this.state.position', this.state.position);
});
this.konva.transformer.on('transform', () => {
// Always snap the interaction rect to the nearest pixel when transforming
- const x = Math.round(this.konva.interactionRect.x());
- const y = Math.round(this.konva.interactionRect.y());
- // Snap its position
- this.konva.interactionRect.x(x);
- this.konva.interactionRect.y(y);
+ // const x = Math.round(this.konva.interactionRect.x());
+ // const y = Math.round(this.konva.interactionRect.y());
+ // // Snap its position
+ // this.konva.interactionRect.x(x);
+ // this.konva.interactionRect.y(y);
- // Calculate the new scale of the interaction rect such that its width and height snap to the nearest pixel
- const targetWidth = Math.max(
- Math.round(this.konva.interactionRect.width() * Math.abs(this.konva.interactionRect.scaleX())),
- MIN_LAYER_SIZE_PX
- );
- const scaleX = targetWidth / this.konva.interactionRect.width();
- const targetHeight = Math.max(
- Math.round(this.konva.interactionRect.height() * Math.abs(this.konva.interactionRect.scaleY())),
- MIN_LAYER_SIZE_PX
- );
- const scaleY = targetHeight / this.konva.interactionRect.height();
+ // // Calculate the new scale of the interaction rect such that its width and height snap to the nearest pixel
+ // const targetWidth = Math.max(
+ // Math.round(this.konva.interactionRect.width() * Math.abs(this.konva.interactionRect.scaleX())),
+ // MIN_LAYER_SIZE_PX
+ // );
+ // const scaleX = targetWidth / this.konva.interactionRect.width();
+ // const targetHeight = Math.max(
+ // Math.round(this.konva.interactionRect.height() * Math.abs(this.konva.interactionRect.scaleY())),
+ // MIN_LAYER_SIZE_PX
+ // );
+ // const scaleY = targetHeight / this.konva.interactionRect.height();
- // Snap the width and height (via scale) of the interaction rect
- this.konva.interactionRect.scaleX(scaleX);
- this.konva.interactionRect.scaleY(scaleY);
- this.konva.interactionRect.rotation(0);
+ // // Snap the width and height (via scale) of the interaction rect
+ // this.konva.interactionRect.scaleX(scaleX);
+ // this.konva.interactionRect.scaleY(scaleY);
+ // this.konva.interactionRect.rotation(0);
console.log('>>> transform');
console.log('activeAnchor', this.konva.transformer.getActiveAnchor());
@@ -140,119 +134,20 @@ export class CanvasLayer {
rotation: this.konva.interactionRect.rotation(),
});
- // Handle anchor-specific transformations of the layer's objects
- const anchor = this.konva.transformer.getActiveAnchor();
- // 'top-left'
- // 'top-center'
- // 'top-right'
- // 'middle-right'
- // 'middle-left'
- // 'bottom-left'
- // 'bottom-center'
- // 'bottom-right'
- if (anchor === 'middle-right') {
- // Dragging the anchor to the right
- this.konva.objectGroup.setAttrs({
- scaleX,
- x: x - (x - this.state.position.x) * scaleX,
- });
- } else if (anchor === 'middle-left') {
- // Dragging the anchor to the right
- this.konva.objectGroup.setAttrs({
- scaleX,
- x: x - (x - this.state.position.x) * scaleX,
- });
- } else if (anchor === 'bottom-center') {
- // Resize the interaction rect downwards
- this.konva.objectGroup.setAttrs({
- scaleY,
- y: y - (y - this.state.position.y) * scaleY,
- });
- } else if (anchor === 'bottom-right') {
- // Resize the interaction rect to the right and downwards via scale
- this.konva.objectGroup.setAttrs({
- scaleX,
- scaleY,
- x: x - (x - this.state.position.x) * scaleX,
- y: y - (y - this.state.position.y) * scaleY,
- });
- } else if (anchor === 'top-center') {
- // Resize the interaction rect to the upwards via scale & y position
- this.konva.objectGroup.setAttrs({
- y,
- scaleY,
- });
- }
- this.konva.objectGroupBbox.setAttrs({
- x: this.konva.objectGroup.x(),
- y: this.konva.objectGroup.y(),
- rotation: this.konva.objectGroup.rotation(),
- scaleX: this.konva.objectGroup.scaleX(),
- scaleY: this.konva.objectGroup.scaleY(),
+ this.konva.objectGroup.setAttrs({
+ x: this.konva.interactionRect.x(),
+ y: this.konva.interactionRect.y(),
+ scaleX: this.konva.interactionRect.scaleX(),
+ scaleY: this.konva.interactionRect.scaleY(),
+ rotation: this.konva.interactionRect.rotation(),
});
});
- // this.konva.transformer.on('transform', () => {
- // // We need to snap the transform to the nearest pixel - both the position and the scale
-
- // // Snap the interaction rect to the nearest pixel
- // this.konva.interactionRect.x(Math.round(this.konva.interactionRect.x()));
- // this.konva.interactionRect.y(Math.round(this.konva.interactionRect.y()));
-
- // // Calculate the new scale of the interaction rect such that its width and height snap to the nearest pixel
- // const roundedScaledWidth = Math.round(this.konva.interactionRect.width() * this.konva.interactionRect.scaleX());
- // const correctedScaleX = roundedScaledWidth / this.konva.interactionRect.width();
- // const roundedScaledHeight = Math.round(this.konva.interactionRect.height() * this.konva.interactionRect.scaleY());
- // const correctedScaleY = roundedScaledHeight / this.konva.interactionRect.height();
-
- // // Update the interaction rect's scale to the corrected scale
- // this.konva.interactionRect.scaleX(correctedScaleX);
- // this.konva.interactionRect.scaleY(correctedScaleY);
-
- // console.log('>>> transform');
- // console.log('activeAnchor', this.konva.transformer.getActiveAnchor());
- // console.log('interactionRect', {
- // x: this.konva.interactionRect.x(),
- // y: this.konva.interactionRect.y(),
- // scaleX: this.konva.interactionRect.scaleX(),
- // scaleY: this.konva.interactionRect.scaleY(),
- // width: this.konva.interactionRect.width(),
- // height: this.konva.interactionRect.height(),
- // rotation: this.konva.interactionRect.rotation(),
- // });
-
- // // Update the object group to reflect the new scale and position of the interaction rect
- // this.konva.objectGroup.setAttrs({
- // // The scale is the same as the interaction rect
- // scaleX: this.konva.interactionRect.scaleX(),
- // scaleY: this.konva.interactionRect.scaleY(),
- // rotation: this.konva.interactionRect.rotation(),
- // // We need to do some compensation for the new position. The bounds of the object group may be different from the
- // // interaction rect/bbox, because the object group may have eraser strokes that are not included in the bbox.
- // x:
- // this.konva.interactionRect.x() -
- // Math.abs(this.konva.interactionRect.x() - this.state.position.x) * this.konva.interactionRect.scaleX(),
- // y:
- // this.konva.interactionRect.y() -
- // Math.abs(this.konva.interactionRect.y() - this.state.position.y) * this.konva.interactionRect.scaleY(),
- // // x: this.konva.interactionRect.x() + (this.konva.interactionRect.x() - this.state.position.x) * this.konva.interactionRect.scaleX(),
- // // y: this.konva.interactionRect.y() + (this.konva.interactionRect.y() - this.state.position.y) * this.konva.interactionRect.scaleY(),
- // });
- // this.konva.objectGroupBbox.setAttrs({
- // x: this.konva.objectGroup.x(),
- // y: this.konva.objectGroup.y(),
- // scaleX: this.konva.objectGroup.scaleX(),
- // scaleY: this.konva.objectGroup.scaleY(),
- // });
- // });
this.konva.transformer.on('transformend', () => {
- this.bbox = {
- x: this.konva.interactionRect.x(),
- y: this.konva.interactionRect.y(),
- width: Math.round(this.konva.interactionRect.width() * this.konva.interactionRect.scaleX()),
- 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,
@@ -260,6 +155,7 @@ export class CanvasLayer {
// },
// 'layer'
// );
+ this.logBbox('transformend bbox');
});
this.konva.interactionRect.on('dragmove', () => {
@@ -277,19 +173,15 @@ export class CanvasLayer {
// 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.state.position.x + this.konva.interactionRect.x() - this.bbox.x,
- y: this.state.position.y + this.konva.interactionRect.y() - this.bbox.y,
+ x: this.konva.interactionRect.x(),
+ y: this.konva.interactionRect.y(),
});
-
- const rect = this.konva.objectGroup.getClientRect({ skipTransform: true });
- this.konva.objectGroupBbox.setAttrs({ ...rect, x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() });
});
this.konva.interactionRect.on('dragend', () => {
- // Update the bbox
- this.bbox.x = this.konva.interactionRect.x();
- this.bbox.y = this.konva.interactionRect.y();
+ this.logBbox('dragend bbox');
// Update internal state
+ // this.state.position = { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() };
this.manager.stateApi.onPosChanged(
{
id: this.id,
@@ -302,7 +194,10 @@ export class CanvasLayer {
this.objects = new Map();
this.drawingBuffer = null;
this.state = state;
- this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
+ this.offsetX = 0;
+ this.offsetY = 0;
+ this.width = 0;
+ this.height = 0;
console.log(this);
}
@@ -319,6 +214,32 @@ 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;
@@ -344,27 +265,7 @@ export class CanvasLayer {
}
async render(state: LayerEntity) {
- this.state = state;
-
- // Update the layer's position and listening state
- this.konva.objectGroup.setAttrs({
- x: state.position.x,
- y: state.position.y,
- scaleX: 1,
- scaleY: 1,
- });
- this.konva.positionXLine.points([
- state.position.x,
- -this.manager.stage.y(),
- state.position.x,
- this.manager.stage.y() + this.manager.stage.height() / this.manager.stage.scaleY(),
- ]);
- this.konva.positionYLine.points([
- -this.manager.stage.x(),
- state.position.y,
- this.manager.stage.x() + this.manager.stage.width() / this.manager.stage.scaleX(),
- state.position.y,
- ]);
+ this.state = deepClone(state);
let didDraw = false;
@@ -465,9 +366,12 @@ export class CanvasLayer {
if (didDraw) {
if (this.objects.size > 0) {
- // this.getBbox();
+ this.getBbox();
} else {
- this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
+ this.offsetX = 0;
+ this.offsetY = 0;
+ this.width = 0;
+ this.height = 0;
this.renderBbox();
}
}
@@ -475,15 +379,14 @@ export class CanvasLayer {
this.konva.layer.visible(true);
this.konva.objectGroup.opacity(this.state.opacity);
const isSelected = this.manager.stateApi.getIsSelected(this.id);
- const selectedTool = this.manager.stateApi.getToolState().selected;
+ const toolState = this.manager.stateApi.getToolState();
- const isTransforming = selectedTool === 'transform' && isSelected;
- const isMoving = selectedTool === 'move' && isSelected;
+ const isMoving = toolState.selected === 'move' && isSelected;
- this.konva.layer.listening(isTransforming || isMoving);
- this.konva.transformer.listening(isTransforming);
+ this.konva.layer.listening(toolState.isTransforming || isMoving);
+ this.konva.transformer.listening(toolState.isTransforming);
this.konva.bbox.visible(isMoving);
- this.konva.interactionRect.listening(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.
@@ -491,7 +394,7 @@ export class CanvasLayer {
if (this.konva.objectGroup.isCached()) {
this.konva.objectGroup.clearCache();
}
- } else if (isSelected && selectedTool === 'transform') {
+ } 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) {
@@ -501,7 +404,7 @@ export class CanvasLayer {
this.konva.transformer.nodes([this.konva.interactionRect]);
this.konva.transformer.forceUpdate();
this.konva.transformer.visible(true);
- } else if (selectedTool === 'move') {
+ } 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) {
@@ -515,7 +418,7 @@ export class CanvasLayer {
// 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(selectedTool)) {
+ 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()) {
@@ -540,43 +443,23 @@ export class CanvasLayer {
}
renderBbox() {
+ const toolState = this.manager.stateApi.getToolState();
+ if (toolState.isTransforming) {
+ return;
+ }
const isSelected = this.manager.stateApi.getIsSelected(this.id);
- const selectedTool = this.manager.stateApi.getToolState().selected;
- const scale = this.manager.stage.scaleX();
- const hasBbox = this.bbox.width !== 0 && this.bbox.height !== 0;
-
- this.konva.bbox.visible(hasBbox && isSelected && selectedTool === 'move');
+ const hasBbox = this.width !== 0 && this.height !== 0;
+ this.konva.bbox.visible(hasBbox && isSelected && toolState.selected === 'move');
this.konva.interactionRect.visible(hasBbox);
- const rect = this.konva.objectGroup.getClientRect({ skipTransform: true });
- this.konva.objectGroupBbox.setAttrs({
- ...rect,
- x: this.konva.objectGroup.x(),
- y: this.konva.objectGroup.y(),
- scaleX: 1,
- scaleY: 1,
- });
- this.konva.bbox.setAttrs({
- x: this.bbox.x - CanvasLayer.BBOX_PADDING_PX / scale,
- y: this.bbox.y - CanvasLayer.BBOX_PADDING_PX / scale,
- width: this.bbox.width + (CanvasLayer.BBOX_PADDING_PX / scale) * 2,
- height: this.bbox.height + (CanvasLayer.BBOX_PADDING_PX / scale) * 2,
- scaleX: 1,
- scaleY: 1,
- strokeWidth: 1 / this.manager.stage.scaleX(),
- });
- this.konva.interactionRect.setAttrs({
- x: this.bbox.x,
- y: this.bbox.y,
- width: this.bbox.width,
- height: this.bbox.height,
- scaleX: 1,
- scaleY: 1,
- });
+ this.updatePosition();
}
private _getBbox() {
if (this.objects.size === 0) {
- this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
+ this.offsetX = 0;
+ this.offsetY = 0;
+ this.width = 0;
+ this.height = 0;
this.renderBbox();
return;
}
@@ -595,16 +478,21 @@ export class CanvasLayer {
}
if (!needsPixelBbox) {
- if (rect.width === 0 || rect.height === 0) {
- this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
- } else {
- this.bbox = {
- x: this.konva.objectGroup.x() + rect.x,
- y: this.konva.objectGroup.y() + rect.y,
- width: rect.width,
- height: rect.height,
- };
- }
+ this.offsetX = rect.x;
+ 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();
return;
}
@@ -621,21 +509,34 @@ export class CanvasLayer {
this.manager.requestBbox(
{ buffer: imageData.data.buffer, width: imageData.width, height: imageData.height },
(extents) => {
+ console.log('extents', extents);
if (extents) {
const { minX, minY, maxX, maxY } = extents;
- this.bbox = {
- x: this.konva.objectGroup.x() + rect.x + minX,
- y: this.konva.objectGroup.y() + rect.y + minY,
- width: maxX - minX,
- height: maxY - minY,
- };
+ this.offsetX = minX + rect.x;
+ this.offsetY = minY + rect.y;
+ this.width = maxX - minX;
+ this.height = maxY - minY;
} else {
- this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
+ this.offsetX = 0;
+ this.offsetY = 0;
+ this.width = 0;
+ this.height = 0;
}
- console.log('new bbox', deepClone(this.bbox));
+ this.logBbox('new bbox from worker');
this.renderBbox();
clone.destroy();
}
);
}
+
+ logBbox(msg: string = 'bbox') {
+ console.log(msg, {
+ x: this.state.position.x,
+ y: this.state.position.y,
+ offsetX: this.offsetX,
+ offsetY: this.offsetY,
+ width: this.width,
+ height: this.height,
+ });
+ }
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts
index 41291ee80b..ea7452343c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts
@@ -160,7 +160,7 @@ export class CanvasTool {
} else if (!isDrawableEntity) {
// Non-drawable layers don't have tools
stage.container().style.cursor = 'not-allowed';
- } else if (tool === 'move') {
+ } else if (tool === 'move' || toolState.isTransforming) {
// Move tool gets a pointer
stage.container().style.cursor = 'default';
} else if (tool === 'rect') {
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts
index 30e673cdc3..15b816f8e7 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts
@@ -194,6 +194,7 @@ export const {
allEntitiesDeleted,
clipToBboxChanged,
canvasReset,
+ toolIsTransformingChanged,
// bbox
bboxChanged,
bboxScaledSizeChanged,
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts
index c1f14d7df4..3724f4942b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts
@@ -20,4 +20,7 @@ export const toolReducers = {
toolBufferChanged: (state, action: PayloadAction) => {
state.tool.selectedBuffer = action.payload;
},
+ toolIsTransformingChanged: (state, action: PayloadAction) => {
+ state.tool.isTransforming = action.payload;
+ },
} satisfies SliceCaseReducers;
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index 06b7bfa064..a6aa5936f8 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -464,7 +464,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
},
};
-const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view', 'bbox', 'transform']);
+const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view', 'bbox']);
export type Tool = z.infer;
export function isDrawingTool(tool: Tool): tool is 'brush' | 'eraser' | 'rect' {
return tool === 'brush' || tool === 'eraser' || tool === 'rect';
@@ -850,6 +850,7 @@ export type CanvasV2State = {
brush: { width: number };
eraser: { width: number };
fill: RgbaColor;
+ isTransforming: boolean;
};
settings: {
imageSmoothing: boolean;