diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index c33357efa7..cd3be44e93 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1738,6 +1738,8 @@ "unlocked": "Unlocked", "deleteSelected": "Delete Selected", "deleteAll": "Delete All", + "flipHorizontal": "Flip Horizontal", + "flipVertical": "Flip Vertical", "fill": { "fillStyle": "Fill Style", "solid": "Solid", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx index fabc93604c..494a5a4ba0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx @@ -32,14 +32,14 @@ export const CanvasEditor = memo(() => { > - + - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Transform.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Transform.tsx index 84783d0fb8..d6b84cc203 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Transform.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Transform.tsx @@ -6,9 +6,15 @@ import { useEntityIdentifierContext, } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { useEntityAdapter } from 'features/controlLayers/hooks/useEntityAdapter'; -import { memo, useCallback } from 'react'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiCheckBold, PiXBold } from 'react-icons/pi'; +import { + PiArrowsCounterClockwiseBold, + PiCheckBold, + PiFlipHorizontalFill, + PiFlipVerticalFill, + PiXBold, +} from 'react-icons/pi'; const TransformBox = memo(() => { const { t } = useTranslation(); @@ -16,14 +22,6 @@ const TransformBox = memo(() => { const adapter = useEntityAdapter(entityIdentifier); const isProcessing = useStore(adapter.transformer.$isProcessing); - const applyTransform = useCallback(() => { - adapter.transformer.applyTransform(); - }, [adapter.transformer]); - - const cancelFilter = useCallback(() => { - adapter.transformer.stopTransform(); - }, [adapter.transformer]); - return ( { {t('controlLayers.tool.transform')} + + + + + - diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts index 846b9c5915..8d556a43e9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts @@ -259,13 +259,7 @@ export class CanvasEntityTransformer extends CanvasModuleABC { // This is called when a transform anchor is dragged. By this time, the transform constraints in the above // callbacks have been enforced, and the transformer has updated its nodes' attributes. We need to pass the // updated attributes to the object group, propagating the transformation on down. - this.parent.renderer.konva.objectGroup.setAttrs({ - x: this.konva.proxyRect.x(), - y: this.konva.proxyRect.y(), - scaleX: this.konva.proxyRect.scaleX(), - scaleY: this.konva.proxyRect.scaleY(), - rotation: this.konva.proxyRect.rotation(), - }); + this.syncObjectGroupWithProxyRect(); }); this.konva.transformer.on('transformend', () => { @@ -395,6 +389,53 @@ export class CanvasEntityTransformer extends CanvasModuleABC { this.parent.konva.layer.add(this.konva.transformer); } + flipHorizontal = () => { + if (!this.isTransforming || this.$isProcessing.get()) { + return; + } + + // Flipping horizontally = flipping across the vertical axis: + // - Flip by negating the x scale + // - Restore position by translating the rect rightwards by the width of the rect + const x = this.konva.proxyRect.x(); + const width = this.konva.proxyRect.width(); + const scaleX = this.konva.proxyRect.scaleX(); + this.konva.proxyRect.setAttrs({ + scaleX: -scaleX, + x: x + width * scaleX, + }); + + this.syncObjectGroupWithProxyRect(); + }; + + flipVertical = () => { + if (!this.isTransforming || this.$isProcessing.get()) { + return; + } + + // Flipping vertically = flipping across the horizontal axis: + // - Flip by negating the y scale + // - Restore position by translating the rect downwards by the height of the rect + const y = this.konva.proxyRect.y(); + const height = this.konva.proxyRect.height(); + const scaleY = this.konva.proxyRect.scaleY(); + this.konva.proxyRect.setAttrs({ + scaleY: -scaleY, + y: y + height * scaleY, + }); + this.syncObjectGroupWithProxyRect(); + }; + + syncObjectGroupWithProxyRect = () => { + this.parent.renderer.konva.objectGroup.setAttrs({ + x: this.konva.proxyRect.x(), + y: this.konva.proxyRect.y(), + scaleX: this.konva.proxyRect.scaleX(), + scaleY: this.konva.proxyRect.scaleY(), + rotation: this.konva.proxyRect.rotation(), + }); + }; + /** * Updates the transformer's visual components to match the parent entity's position and bounding box. * @param position The position of the parent entity @@ -510,6 +551,12 @@ export class CanvasEntityTransformer extends CanvasModuleABC { this.stopTransform(); }; + resetTransform = () => { + this.resetScale(); + this.updatePosition(); + this.updateBbox(); + }; + /** * Stops the transformation of the entity. If the transformation is in progress, the entity will be reset to its * original state. @@ -520,12 +567,9 @@ export class CanvasEntityTransformer extends CanvasModuleABC { this.isTransforming = false; this.setInteractionMode('off'); - // Reset the scale of the the entity. We've either replaced the transformed objects with a rasterized image, or + // Reset the transform of the the entity. We've either replaced the transformed objects with a rasterized image, or // canceled a transformation. In either case, the scale should be reset. - this.resetScale(); - - this.updatePosition(); - this.updateBbox(); + this.resetTransform(); this.syncInteractionState(); this.manager.stateApi.$transformingEntity.set(null); this.$isProcessing.set(false);