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;