diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInteractionRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInteractionRect.ts deleted file mode 100644 index 77c9a57d62..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInteractionRect.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; -import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { GetLoggingContext } from 'features/controlLayers/store/types'; -import Konva from 'konva'; -import type { Logger } from 'roarr'; - -export class CanvasInteractionRect { - static TYPE = 'interaction_rect'; - - id: string; - parent: CanvasLayer; - manager: CanvasManager; - log: Logger; - getLoggingContext: GetLoggingContext; - - konva: { - rect: Konva.Rect; - }; - - constructor(parent: CanvasLayer) { - this.id = getPrefixedId(CanvasInteractionRect.TYPE); - this.parent = parent; - this.manager = parent.manager; - - this.getLoggingContext = this.manager.buildObjectGetLoggingContext(this); - this.log = this.manager.buildLogger(this.getLoggingContext); - - this.konva = { - rect: new Konva.Rect({ - name: CanvasInteractionRect.TYPE, - listening: false, - draggable: true, - // fill: 'rgba(255,0,0,0.5)', - }), - }; - - this.konva.rect.on('dragmove', () => { - // Snap the interaction rect to the nearest pixel - this.konva.rect.x(Math.round(this.konva.rect.x())); - this.konva.rect.y(Math.round(this.konva.rect.y())); - - // The bbox should be updated to reflect the new position of the interaction rect, taking into account its padding - // and border - this.parent.konva.bbox.setAttrs({ - x: this.konva.rect.x() - this.manager.getScaledBboxPadding(), - y: this.konva.rect.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.parent.konva.objectGroup.setAttrs({ - x: this.konva.rect.x(), - y: this.konva.rect.y(), - }); - }); - this.konva.rect.on('dragend', () => { - if (this.parent.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; - } - - const position = { - x: this.konva.rect.x() - this.parent.bbox.x, - y: this.konva.rect.y() - this.parent.bbox.y, - }; - - this.log.trace({ position }, 'Position changed'); - this.manager.stateApi.onPosChanged({ id: this.id, position }, 'layer'); - }); - } - - repr = () => { - return { - id: this.id, - type: CanvasInteractionRect.TYPE, - }; - }; -} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index c78eb57a81..de19c2199e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -46,7 +46,6 @@ export class CanvasLayer { layer: Konva.Layer; bbox: Konva.Rect; objectGroup: Konva.Group; - interactionRect: Konva.Rect; }; objects: Map; transformer: CanvasTransformer; @@ -77,56 +76,15 @@ export class CanvasLayer { strokeHitEnabled: false, }), objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }), - interactionRect: new Konva.Rect({ - name: CanvasLayer.INTERACTION_RECT_NAME, - listening: false, - draggable: true, - // fill: 'rgba(255,0,0,0.5)', - }), }; this.transformer = new CanvasTransformer(this); this.konva.layer.add(this.konva.objectGroup); this.konva.layer.add(this.transformer.konva.transformer); - this.konva.layer.add(this.konva.interactionRect); + this.konva.layer.add(this.transformer.konva.proxyRect); this.konva.layer.add(this.konva.bbox); - this.konva.interactionRect.on('dragmove', () => { - // 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())); - - // 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() - 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(), - }); - }); - this.konva.interactionRect.on('dragend', () => { - 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; - } - - const position = { - x: this.konva.interactionRect.x() - this.bbox.x, - y: this.konva.interactionRect.y() - this.bbox.y, - }; - - this.log.trace({ position }, 'Position changed'); - this.manager.stateApi.onPosChanged({ id: this.id, position }, 'layer'); - }); - this.objects = new Map(); this.drawingBuffer = null; this.state = state; @@ -235,9 +193,9 @@ export class CanvasLayer { x: position.x + this.bbox.x - bboxPadding, y: position.y + this.bbox.y - bboxPadding, }); - this.konva.interactionRect.setAttrs({ - x: position.x + this.bbox.x * this.konva.interactionRect.scaleX(), - y: position.y + this.bbox.y * this.konva.interactionRect.scaleY(), + this.transformer.konva.proxyRect.setAttrs({ + x: position.x + this.bbox.x * this.transformer.konva.proxyRect.scaleX(), + y: position.y + this.bbox.y * this.transformer.konva.proxyRect.scaleY(), }); }; @@ -301,22 +259,23 @@ export class CanvasLayer { this.konva.layer.listening(true); // The transformer is not needed - this.transformer.deactivate(); + this.transformer.disableTransform(); + this.transformer.enableDrag(); // 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); - if (listening) { - this.transformer.activate(); + if (toolState.selected !== 'view') { + this.konva.layer.listening(true); + this.transformer.enableTransform(); + this.transformer.enableDrag(); } else { - this.transformer.deactivate(); + this.konva.layer.listening(false); + this.transformer.disableTransform(); + this.transformer.disableDrag(); } // Hide the bbox rect, the transformer will has its own bbox @@ -326,9 +285,9 @@ export class CanvasLayer { this.konva.layer.listening(false); // The transformer, bbox and interaction rect should be inactive - this.transformer.deactivate(); + this.transformer.disableTransform(); + this.transformer.disableDrag(); this.konva.bbox.visible(false); - this.konva.interactionRect.listening(false); } }; @@ -348,12 +307,13 @@ export class CanvasLayer { this.manager.stateApi.onEntityReset({ id: this.id }, 'layer'); } this.konva.bbox.visible(false); - this.konva.interactionRect.visible(false); + this.transformer.disableDrag(); + this.transformer.disableTransform(); return; } this.konva.bbox.visible(true); - this.konva.interactionRect.visible(true); + this.transformer.enableDrag(); const onePixel = this.manager.getScaledPixel(); const bboxPadding = this.manager.getScaledBboxPadding(); @@ -365,7 +325,7 @@ export class CanvasLayer { height: this.bbox.height + bboxPadding * 2, strokeWidth: onePixel, }); - this.konva.interactionRect.setAttrs({ + this.transformer.konva.proxyRect.setAttrs({ x: this.state.position.x + this.bbox.x, y: this.state.position.y + this.bbox.y, width: this.bbox.width, @@ -386,10 +346,10 @@ export class CanvasLayer { 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, + x: this.transformer.konva.proxyRect.x() - bboxPadding, + y: this.transformer.konva.proxyRect.y() - bboxPadding, + width: this.transformer.konva.proxyRect.width() * this.transformer.konva.proxyRect.scaleX() + bboxPadding * 2, + height: this.transformer.konva.proxyRect.height() * this.transformer.konva.proxyRect.scaleY() + bboxPadding * 2, strokeWidth: onePixel, }); }; @@ -465,8 +425,8 @@ export class CanvasLayer { const listening = this.manager.stateApi.getToolState().selected !== 'view'; this.konva.layer.listening(listening); - this.konva.interactionRect.listening(listening); - this.transformer.activate(); + this.transformer.enableDrag(); + this.transformer.enableTransform(); // Hide the bbox rect, the transformer will has its own bbox this.konva.bbox.visible(false); @@ -480,14 +440,14 @@ export class CanvasLayer { }; this.konva.objectGroup.setAttrs(attrs); this.konva.bbox.setAttrs(attrs); - this.konva.interactionRect.setAttrs(attrs); + this.transformer.konva.proxyRect.setAttrs(attrs); }; rasterizeLayer = async () => { this.log.debug('Rasterizing layer'); const objectGroupClone = this.konva.objectGroup.clone(); - const interactionRectClone = this.konva.interactionRect.clone(); + const interactionRectClone = this.transformer.konva.proxyRect.clone(); const rect = interactionRectClone.getClientRect(); const blob = await konvaNodeToBlob(objectGroupClone, rect); if (this.manager._isDebugging) { @@ -620,13 +580,13 @@ export class CanvasLayer { const info = { repr: this.repr(), interactionRectAttrs: { - 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(), + x: this.transformer.konva.proxyRect.x(), + y: this.transformer.konva.proxyRect.y(), + scaleX: this.transformer.konva.proxyRect.scaleX(), + scaleY: this.transformer.konva.proxyRect.scaleY(), + width: this.transformer.konva.proxyRect.width(), + height: this.transformer.konva.proxyRect.height(), + rotation: this.transformer.konva.proxyRect.rotation(), }, objectGroupAttrs: { x: this.konva.objectGroup.x(), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 63ff1204c6..04fa9c8be4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -6,7 +6,6 @@ import type { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLi import type { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'; import type { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import { CanvasInitialImage } from 'features/controlLayers/konva/CanvasInitialImage'; -import type { CanvasInteractionRect } from 'features/controlLayers/konva/CanvasInteractionRect'; import { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview'; import type { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import type { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; @@ -594,7 +593,7 @@ export class CanvasManager { } buildObjectGetLoggingContext = ( - instance: CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage | CanvasTransformer | CanvasInteractionRect + instance: CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage | CanvasTransformer ): GetLoggingContext => { return (extra?: JSONObject): JSONObject => { return { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index c34099193b..7458ee6a0e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -1,22 +1,27 @@ import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { Coordinate , GetLoggingContext } from 'features/controlLayers/store/types'; +import type { Coordinate, GetLoggingContext } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; export class CanvasTransformer { - static TYPE = 'transformer'; + static TYPE = 'entity_transformer'; + static TRANSFORMER_NAME = `${CanvasTransformer.TYPE}:transformer`; + static PROXY_RECT_NAME = `${CanvasTransformer.TYPE}:proxy_rect`; id: string; - parent: CanvasLayer + parent: CanvasLayer; manager: CanvasManager; log: Logger; - getLoggingContext: GetLoggingContext + getLoggingContext: GetLoggingContext; + + isTransformEnabled: boolean; + isDragEnabled: boolean; - isActive: boolean; konva: { transformer: Konva.Transformer; + proxyRect: Konva.Rect; }; constructor(parent: CanvasLayer) { @@ -27,12 +32,12 @@ export class CanvasTransformer { this.getLoggingContext = this.manager.buildObjectGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); - this.isActive = false; + this.isTransformEnabled = false; + this.isDragEnabled = false; + this.konva = { transformer: new Konva.Transformer({ - name: CanvasTransformer.TYPE, - // The transformer will use the interaction rect as a proxy for the entity it is transforming. - nodes: [parent.konva.interactionRect], + name: CanvasTransformer.TRANSFORMER_NAME, // Visibility and listening are managed via activate() and deactivate() visible: false, listening: false, @@ -113,17 +118,22 @@ export class CanvasTransformer { return newBoundBox; }, }), + proxyRect: new Konva.Rect({ + name: CanvasTransformer.PROXY_RECT_NAME, + listening: false, + draggable: true, + }), }; this.konva.transformer.on('transformstart', () => { // Just logging in this callback. Called on mouse down of a transform anchor. this.log.trace( { - x: parent.konva.interactionRect.x(), - y: parent.konva.interactionRect.y(), - scaleX: parent.konva.interactionRect.scaleX(), - scaleY: parent.konva.interactionRect.scaleY(), - rotation: parent.konva.interactionRect.rotation(), + 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(), }, 'Transform started' ); @@ -134,11 +144,11 @@ export class CanvasTransformer { // 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. parent.konva.objectGroup.setAttrs({ - x: parent.konva.interactionRect.x(), - y: parent.konva.interactionRect.y(), - scaleX: parent.konva.interactionRect.scaleX(), - scaleY: parent.konva.interactionRect.scaleY(), - rotation: parent.konva.interactionRect.rotation(), + 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(), }); }); @@ -146,18 +156,18 @@ export class CanvasTransformer { // Called on mouse up on an anchor. We'll do some final snapping to ensure the transformer is pixel-perfect. // Snap the position to the nearest pixel. - const x = parent.konva.interactionRect.x(); - const y = parent.konva.interactionRect.y(); + const x = this.konva.proxyRect.x(); + const y = this.konva.proxyRect.y(); const snappedX = Math.round(x); const snappedY = Math.round(y); // The transformer doesn't modify the width and height. It only modifies scale. We'll need to apply the scale to // the width and height, round them to the nearest pixel, and finally calculate a new scale that will result in // the snapped width and height. - const width = parent.konva.interactionRect.width(); - const height = parent.konva.interactionRect.height(); - const scaleX = parent.konva.interactionRect.scaleX(); - const scaleY = parent.konva.interactionRect.scaleY(); + const width = this.konva.proxyRect.width(); + const height = this.konva.proxyRect.height(); + const scaleX = this.konva.proxyRect.scaleX(); + const scaleY = this.konva.proxyRect.scaleY(); // Determine the target width and height, rounded to the nearest pixel. Must be >= 1. Because the scales can be // negative, we need to take the absolute value of the width and height. @@ -169,7 +179,7 @@ export class CanvasTransformer { const snappedScaleY = (targetHeight / height) * Math.sign(scaleY); // Update interaction rect and object group attributes. - parent.konva.interactionRect.setAttrs({ + this.konva.proxyRect.setAttrs({ x: snappedX, y: snappedY, scaleX: snappedScaleX, @@ -183,7 +193,7 @@ export class CanvasTransformer { }); // Rotation is only retrieved for logging purposes. - const rotation = parent.konva.interactionRect.rotation(); + const rotation = this.konva.proxyRect.rotation(); this.log.trace( { @@ -205,6 +215,41 @@ export class CanvasTransformer { ); }); + this.konva.proxyRect.on('dragmove', () => { + // Snap the interaction rect to the nearest pixel + this.konva.proxyRect.x(Math.round(this.konva.proxyRect.x())); + this.konva.proxyRect.y(Math.round(this.konva.proxyRect.y())); + + // The bbox should be updated to reflect the new position of the interaction rect, taking into account its padding + // and border + this.parent.konva.bbox.setAttrs({ + x: this.konva.proxyRect.x() - this.manager.getScaledBboxPadding(), + y: this.konva.proxyRect.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.parent.konva.objectGroup.setAttrs({ + x: this.konva.proxyRect.x(), + y: this.konva.proxyRect.y(), + }); + }); + this.konva.proxyRect.on('dragend', () => { + if (this.parent.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; + } + + const position = { + x: this.konva.proxyRect.x() - this.parent.bbox.x, + y: this.konva.proxyRect.y() - this.parent.bbox.y, + }; + + this.log.trace({ position }, 'Position changed'); + this.manager.stateApi.onPosChanged({ id: this.id, position }, 'layer'); + }); + this.manager.stateApi.onShiftChanged((isPressed) => { // While the user holds shift, we want to snap rotation to 45 degree increments. Listen for the shift key state // and update the snap angles accordingly. @@ -212,29 +257,37 @@ export class CanvasTransformer { }); } - /** - * Activate the transformer. This will make it visible and listening for events. - */ - activate = () => { - this.isActive = true; + enableTransform = () => { + this.isTransformEnabled = true; this.konva.transformer.visible(true); this.konva.transformer.listening(true); + this.konva.transformer.nodes([this.konva.proxyRect]); }; - /** - * Deactivate the transformer. This will make it invisible and not listening for events. - */ - deactivate = () => { - this.isActive = false; + disableTransform = () => { + this.isTransformEnabled = false; this.konva.transformer.visible(false); this.konva.transformer.listening(false); + this.konva.transformer.nodes([]); + }; + + enableDrag = () => { + this.isDragEnabled = true; + this.konva.proxyRect.visible(true); + this.konva.proxyRect.listening(true); + }; + + disableDrag = () => { + this.isDragEnabled = false; + this.konva.proxyRect.visible(false); + this.konva.proxyRect.listening(false); }; repr = () => { return { id: this.id, type: CanvasTransformer.TYPE, - isActive: this.isActive, + isActive: this.isTransformEnabled, }; }; }