From f405e472ea9fcefafc951529daf39dcdd5fb596f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 19 Jul 2024 13:16:02 +1000 Subject: [PATCH] fix(ui): move tool fixes, add transform tool --- .../controlLayers/components/ToolChooser.tsx | 2 + .../components/TransformToolButton.tsx | 35 +++++ .../controlLayers/konva/CanvasLayer.ts | 131 ++++++++++++------ 3 files changed, 127 insertions(+), 41 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index 706d51b74c..b9b6c8ca84 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -5,6 +5,7 @@ import { BrushToolButton } from 'features/controlLayers/components/BrushToolButt import { EraserToolButton } from 'features/controlLayers/components/EraserToolButton'; import { MoveToolButton } from 'features/controlLayers/components/MoveToolButton'; import { RectToolButton } from 'features/controlLayers/components/RectToolButton'; +import { TransformToolButton } from 'features/controlLayers/components/TransformToolButton'; import { ViewToolButton } from 'features/controlLayers/components/ViewToolButton'; import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanvasDeleteLayerHotkey'; import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanvasResetLayerHotkey'; @@ -21,6 +22,7 @@ export const ToolChooser: React.FC = () => { + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx new file mode 100644 index 0000000000..e8ac2e2577 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx @@ -0,0 +1,35 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } 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 isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'transform'); + const isDisabled = useAppSelector( + (s) => s.canvasV2.selectedEntityIdentifier === null || s.canvasV2.session.isStaging + ); + + const onClick = useCallback(() => { + dispatch(toolChanged('transform')); + }, [dispatch]); + + useHotkeys(['ctrl+t', 'meta+t'], onClick, { enabled: !isDisabled }, [isDisabled, onClick]); + + return ( + } + variant={isSelected ? 'solid' : 'outline'} + onClick={onClick} + isDisabled={isDisabled} + /> + ); +}); + +TransformToolButton.displayName = 'TransformToolButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index df8392c4f5..e3073df65b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -14,6 +14,7 @@ export class CanvasLayer { static NAME_PREFIX = 'layer'; static LAYER_NAME = `${CanvasLayer.NAME_PREFIX}_layer`; static TRANSFORMER_NAME = `${CanvasLayer.NAME_PREFIX}_transformer`; + static INTERACTION_RECT_NAME = `${CanvasLayer.NAME_PREFIX}_interaction-rect`; static GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_group`; static OBJECT_GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_object-group`; static BBOX_NAME = `${CanvasLayer.NAME_PREFIX}_bbox`; @@ -26,13 +27,15 @@ export class CanvasLayer { konva: { layer: Konva.Layer; - bbox: Konva.Rect; group: Konva.Group; + bbox: Konva.Rect; + objectGroup: Konva.Group; transformer: Konva.Transformer; + interactionRect: Konva.Rect; }; objects: Map; - bbox: Rect | null; + bbox: Rect; getBbox = debounce(this._getBbox, 300); @@ -41,38 +44,63 @@ export class CanvasLayer { this.manager = manager; this.konva = { layer: new Konva.Layer({ id: this.id, name: CanvasLayer.LAYER_NAME, listening: false }), - group: new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: false, draggable: true }), + group: new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: true, draggable: true }), bbox: new Konva.Rect({ listening: false, + draggable: false, name: CanvasLayer.BBOX_NAME, stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400 - fill: '', perfectDrawEnabled: false, strokeHitEnabled: false, }), objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }), transformer: new Konva.Transformer({ name: CanvasLayer.TRANSFORMER_NAME, - shouldOverdrawWholeArea: true, draggable: false, - dragDistance: 0, enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], rotateEnabled: false, flipEnabled: false, listening: false, }), + interactionRect: new Konva.Rect({ + name: CanvasLayer.INTERACTION_RECT_NAME, + listening: false, + draggable: false, + fill: 'rgba(255,0,0,0.5)', + }), }; - this.konva.group.add(this.konva.objectGroup); - this.konva.group.add(this.konva.bbox); this.konva.layer.add(this.konva.group); + this.konva.layer.add(this.konva.transformer); + this.konva.group.add(this.konva.objectGroup); + this.konva.group.add(this.konva.interactionRect); + this.konva.group.add(this.konva.bbox); + this.konva.transformer.on('transform', () => { + console.log(this.konva.interactionRect.position()); + this.konva.objectGroup.setAttrs({ + scaleX: this.konva.interactionRect.scaleX(), + scaleY: this.konva.interactionRect.scaleY(), + // rotation: this.konva.interactionRect.rotation(), + x: this.konva.interactionRect.x(), + t: this.konva.interactionRect.y(), + }); + }); this.konva.transformer.on('transformend', () => { + console.log(this.bbox); + this.bbox = { + 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(), + }; + console.log(this.bbox); + this.renderBbox(); this.manager.stateApi.onScaleChanged( { id: this.id, - scale: this.konva.group.scaleX(), - position: { x: this.konva.group.x(), y: this.konva.group.y() }, + scale: this.konva.objectGroup.scaleX(), + position: { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() }, }, 'layer' ); @@ -83,12 +111,15 @@ export class CanvasLayer { 'layer' ); }); - this.konva.layer.add(this.konva.transformer); this.objects = new Map(); this.drawingBuffer = null; this.state = state; - this.bbox = null; + this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; + } + + private static get DEFAULT_BBOX_RECT() { + return { x: 0, y: 0, width: 0, height: 0 }; } destroy(): void { @@ -235,48 +266,50 @@ export class CanvasLayer { if (this.objects.size > 0) { this.getBbox(); } else { - this.bbox = null; + this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; this.renderBbox(); } } this.konva.layer.visible(true); - this.konva.group.opacity(this.state.opacity); + this.konva.objectGroup.opacity(this.state.opacity); const isSelected = this.manager.stateApi.getIsSelected(this.id); const selectedTool = this.manager.stateApi.getToolState().selected; - const transformerListening = selectedTool === 'transform' && isSelected; - const bboxListening = selectedTool === 'move' && isSelected; + const isTransforming = selectedTool === 'transform' && isSelected; + const isMoving = selectedTool === 'move' && isSelected; - this.konva.layer.listening(transformerListening || bboxListening); - this.konva.transformer.listening(transformerListening); - this.konva.group.listening(bboxListening); - this.konva.bbox.listening(bboxListening); + this.konva.layer.listening(isTransforming || isMoving); + this.konva.transformer.listening(isTransforming); + this.konva.bbox.visible(isMoving); + this.konva.interactionRect.listening(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.group.isCached()) { - this.konva.group.clearCache(); + if (this.konva.objectGroup.isCached()) { + this.konva.objectGroup.clearCache(); } } else if (isSelected && selectedTool === 'transform') { // 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.group.isCached() || didDraw) { - // this.konva.group.cache(); + if (!this.konva.objectGroup.isCached() || didDraw) { + // this.konva.objectGroup.cache(); } // Activate the transformer - this.konva.transformer.nodes([this.konva.group]); + this.konva.transformer.nodes([this.konva.interactionRect]); + this.konva.transformer.enabledAnchors(['top-left', 'top-right', 'bottom-left', 'bottom-right']); this.konva.transformer.forceUpdate(); this.konva.transformer.visible(true); } else if (selectedTool === '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.group.isCached() || didDraw) { - // this.konva.group.cache(); + if (!this.konva.objectGroup.isCached() || didDraw) { + // this.konva.objectGroup.cache(); } // Activate the transformer - this.konva.transformer.nodes([]); + this.konva.transformer.nodes([this.konva.interactionRect]); + this.konva.transformer.enabledAnchors([]); this.konva.transformer.forceUpdate(); this.konva.transformer.visible(false); } else if (isSelected) { @@ -286,14 +319,14 @@ export class CanvasLayer { if (isDrawingTool(selectedTool)) { // 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.group.isCached()) { - this.konva.group.clearCache(); + 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.group.isCached() || didDraw) { - // this.konva.group.cache(); + if (!this.konva.objectGroup.isCached() || didDraw) { + // this.konva.objectGroup.cache(); } } } else if (!isSelected) { @@ -301,8 +334,8 @@ export class CanvasLayer { // 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.group.isCached() || didDraw) { - // this.konva.group.cache(); + if (!this.konva.objectGroup.isCached() || didDraw) { + // this.konva.objectGroup.cache(); } } } @@ -310,17 +343,33 @@ export class CanvasLayer { renderBbox() { const isSelected = this.manager.stateApi.getIsSelected(this.id); const selectedTool = this.manager.stateApi.getToolState().selected; + const hasBbox = this.bbox.width !== 0 && this.bbox.height !== 0; + + this.konva.bbox.visible(hasBbox); + this.konva.interactionRect.visible(hasBbox); this.konva.bbox.setAttrs({ - ...this.bbox, + x: this.bbox.x, + y: this.bbox.y, + width: this.bbox.width, + height: this.bbox.height, + scaleX: 1, + scaleY: 1, strokeWidth: 1 / this.manager.stage.scaleX(), - visible: this.bbox !== null && selectedTool === 'move' && isSelected, + }); + this.konva.interactionRect.setAttrs({ + x: this.bbox.x, + y: this.bbox.y, + width: this.bbox.width, + height: this.bbox.height, + scaleX: 1, + scaleY: 1, }); } private _getBbox() { if (this.objects.size === 0) { - this.bbox = null; + this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; this.renderBbox(); return; } @@ -338,7 +387,7 @@ export class CanvasLayer { if (!needsPixelBbox) { if (rect.width === 0 || rect.height === 0) { - this.bbox = null; + this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; } else { this.bbox = rect; } @@ -370,13 +419,13 @@ export class CanvasLayer { if (extents) { const { minX, minY, maxX, maxY } = extents; this.bbox = { - x: minX + rect.x - Math.floor(this.konva.layer.x()), - y: minY + rect.y - Math.floor(this.konva.layer.y()), + x: rect.x + minX, + y: rect.y + minY, width: maxX - minX, height: maxY - minY, }; } else { - this.bbox = null; + this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; } this.renderBbox(); clone.destroy();