mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): continue modularizing transform
This commit is contained in:
parent
abd22ba087
commit
243feecef9
@ -69,12 +69,10 @@ export class CanvasLayer {
|
||||
objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }),
|
||||
};
|
||||
|
||||
this.transformer = new CanvasTransformer(this);
|
||||
this.transformer = new CanvasTransformer(this, this.konva.objectGroup);
|
||||
|
||||
this.konva.layer.add(this.konva.objectGroup);
|
||||
this.konva.layer.add(this.transformer.konva.transformer);
|
||||
this.konva.layer.add(this.transformer.konva.proxyRect);
|
||||
this.konva.layer.add(this.transformer.konva.bboxOutline);
|
||||
this.konva.layer.add(...this.transformer.getNodes());
|
||||
|
||||
this.objects = new Map();
|
||||
this.drawingBuffer = null;
|
||||
@ -89,6 +87,11 @@ export class CanvasLayer {
|
||||
|
||||
destroy = (): void => {
|
||||
this.log.debug('Destroying layer');
|
||||
// We need to call the destroy method on all children so they can do their own cleanup.
|
||||
this.transformer.destroy();
|
||||
for (const obj of this.objects.values()) {
|
||||
obj.destroy();
|
||||
}
|
||||
this.konva.layer.destroy();
|
||||
};
|
||||
|
||||
@ -172,7 +175,6 @@ export class CanvasLayer {
|
||||
updatePosition = (arg?: { position: Coordinate }) => {
|
||||
this.log.trace('Updating position');
|
||||
const position = get(arg, 'position', this.state.position);
|
||||
const bboxPadding = this.manager.getScaledBboxPadding();
|
||||
|
||||
this.konva.objectGroup.setAttrs({
|
||||
x: position.x + this.bbox.x,
|
||||
@ -180,14 +182,8 @@ export class CanvasLayer {
|
||||
offsetX: this.bbox.x,
|
||||
offsetY: this.bbox.y,
|
||||
});
|
||||
this.transformer.konva.bboxOutline.setAttrs({
|
||||
x: position.x + this.bbox.x - bboxPadding,
|
||||
y: position.y + this.bbox.y - bboxPadding,
|
||||
});
|
||||
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(),
|
||||
});
|
||||
|
||||
this.transformer.update(position, this.bbox);
|
||||
};
|
||||
|
||||
updateObjects = async (arg?: { objects: LayerEntity['objects'] }) => {
|
||||
@ -242,18 +238,17 @@ export class CanvasLayer {
|
||||
if (this.objects.size === 0) {
|
||||
// The layer is totally empty, we can just disable the layer
|
||||
this.konva.layer.listening(false);
|
||||
this.transformer.setMode('off');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelected && !this.isTransforming && toolState.selected === 'move') {
|
||||
// We are moving this layer, it must be listening
|
||||
this.konva.layer.listening(true);
|
||||
|
||||
this.transformer.setMode('drag');
|
||||
} 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
|
||||
// When transforming, we want the stage to still be movable if the view tool is selected. If the transformer is
|
||||
// active, it will interrupt the stage drag events. So we should disable listening when the view tool is selected.
|
||||
if (toolState.selected !== 'view') {
|
||||
this.konva.layer.listening(true);
|
||||
this.transformer.setMode('transform');
|
||||
@ -264,8 +259,6 @@ export class CanvasLayer {
|
||||
} else {
|
||||
// The layer is not selected, or we are using a tool that doesn't need the layer to be listening - disable interaction stuff
|
||||
this.konva.layer.listening(false);
|
||||
|
||||
// The transformer, bbox and interaction rect should be inactive
|
||||
this.transformer.setMode('off');
|
||||
}
|
||||
};
|
||||
@ -290,23 +283,7 @@ export class CanvasLayer {
|
||||
}
|
||||
|
||||
this.transformer.setMode('drag');
|
||||
|
||||
const onePixel = this.manager.getScaledPixel();
|
||||
const bboxPadding = this.manager.getScaledBboxPadding();
|
||||
|
||||
this.transformer.konva.bboxOutline.setAttrs({
|
||||
x: this.state.position.x + this.bbox.x - bboxPadding,
|
||||
y: this.state.position.y + this.bbox.y - bboxPadding,
|
||||
width: this.bbox.width + bboxPadding * 2,
|
||||
height: this.bbox.height + bboxPadding * 2,
|
||||
strokeWidth: onePixel,
|
||||
});
|
||||
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,
|
||||
height: this.bbox.height,
|
||||
});
|
||||
this.transformer.update(this.state.position, this.bbox);
|
||||
this.konva.objectGroup.setAttrs({
|
||||
x: this.state.position.x + this.bbox.x,
|
||||
y: this.state.position.y + this.bbox.y,
|
||||
@ -315,21 +292,6 @@ export class CanvasLayer {
|
||||
});
|
||||
};
|
||||
|
||||
syncStageScale = () => {
|
||||
this.log.trace('Syncing scale to stage');
|
||||
|
||||
const onePixel = this.manager.getScaledPixel();
|
||||
const bboxPadding = this.manager.getScaledBboxPadding();
|
||||
|
||||
this.transformer.konva.bboxOutline.setAttrs({
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
_renderObject = async (obj: LayerEntity['objects'][number], force = false): Promise<boolean> => {
|
||||
if (obj.type === 'brush_line') {
|
||||
let brushLine = this.objects.get(obj.id);
|
||||
|
@ -1,10 +1,19 @@
|
||||
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import type { Subscription } from 'features/controlLayers/konva/util';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import type { Coordinate, GetLoggingContext } from 'features/controlLayers/store/types';
|
||||
import type { Coordinate, GetLoggingContext, Rect } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import type { Logger } from 'roarr';
|
||||
|
||||
/**
|
||||
* The CanvasTransformer class is responsible for managing the transformation of a canvas entity:
|
||||
* - Moving
|
||||
* - Resizing
|
||||
* - Rotating
|
||||
*
|
||||
* It renders an outline when dragging and resizing the entity, with transform anchors for resizing and rotation.
|
||||
*/
|
||||
export class CanvasTransformer {
|
||||
static TYPE = 'entity_transformer';
|
||||
static TRANSFORMER_NAME = `${CanvasTransformer.TYPE}:transformer`;
|
||||
@ -17,24 +26,46 @@ export class CanvasTransformer {
|
||||
manager: CanvasManager;
|
||||
log: Logger;
|
||||
getLoggingContext: GetLoggingContext;
|
||||
subscriptions: Subscription[];
|
||||
|
||||
/**
|
||||
* The current mode of the transformer:
|
||||
* - 'transform': The entity can be moved, resized, and rotated
|
||||
* - 'drag': The entity can only be moved
|
||||
* - 'off': The transformer is disabled
|
||||
*/
|
||||
mode: 'transform' | 'drag' | 'off';
|
||||
|
||||
/**
|
||||
* Whether dragging is enabled. Dragging is enabled in both 'transform' and 'drag' modes.
|
||||
*/
|
||||
isDragEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether transforming is enabled. Transforming is enabled only in 'transform' mode.
|
||||
*/
|
||||
isTransformEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The konva group that the transformer will manipulate.
|
||||
*/
|
||||
transformTarget: Konva.Group;
|
||||
|
||||
konva: {
|
||||
transformer: Konva.Transformer;
|
||||
proxyRect: Konva.Rect;
|
||||
bboxOutline: Konva.Rect;
|
||||
};
|
||||
|
||||
constructor(parent: CanvasLayer) {
|
||||
constructor(parent: CanvasLayer, transformTarget: Konva.Group) {
|
||||
this.id = getPrefixedId(CanvasTransformer.TYPE);
|
||||
this.parent = parent;
|
||||
this.manager = parent.manager;
|
||||
this.id = getPrefixedId(CanvasTransformer.TYPE);
|
||||
this.transformTarget = transformTarget;
|
||||
|
||||
this.getLoggingContext = this.manager.buildObjectGetLoggingContext(this);
|
||||
this.log = this.manager.buildLogger(this.getLoggingContext);
|
||||
this.subscriptions = [];
|
||||
|
||||
this.mode = 'off';
|
||||
this.isDragEnabled = false;
|
||||
@ -156,7 +187,7 @@ export class CanvasTransformer {
|
||||
// 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.
|
||||
parent.konva.objectGroup.setAttrs({
|
||||
this.transformTarget.setAttrs({
|
||||
x: this.konva.proxyRect.x(),
|
||||
y: this.konva.proxyRect.y(),
|
||||
scaleX: this.konva.proxyRect.scaleX(),
|
||||
@ -198,7 +229,7 @@ export class CanvasTransformer {
|
||||
scaleX: snappedScaleX,
|
||||
scaleY: snappedScaleY,
|
||||
});
|
||||
parent.konva.objectGroup.setAttrs({
|
||||
this.transformTarget.setAttrs({
|
||||
x: snappedX,
|
||||
y: snappedY,
|
||||
scaleX: snappedScaleX,
|
||||
@ -242,7 +273,7 @@ export class CanvasTransformer {
|
||||
|
||||
// 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({
|
||||
this.transformTarget.setAttrs({
|
||||
x: this.konva.proxyRect.x(),
|
||||
y: this.konva.proxyRect.y(),
|
||||
});
|
||||
@ -263,13 +294,73 @@ export class CanvasTransformer {
|
||||
this.manager.stateApi.onPosChanged({ id: this.parent.id, position }, 'layer');
|
||||
});
|
||||
|
||||
this.manager.stateApi.onShiftChanged((isPressed) => {
|
||||
this.subscriptions.push(
|
||||
// When the stage scale changes, we may need to re-scale some of the transformer's components. For example,
|
||||
// the bbox outline should always be 1 screen pixel wide, so we need to update its stroke width.
|
||||
this.manager.stateApi.onStageAttrsChanged((newAttrs, oldAttrs) => {
|
||||
if (newAttrs.scale !== oldAttrs?.scale) {
|
||||
this.scale();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.subscriptions.push(
|
||||
// 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.
|
||||
this.konva.transformer.rotationSnaps(isPressed ? [0, 45, 90, 135, 180, 225, 270, 315] : []);
|
||||
});
|
||||
this.manager.stateApi.onShiftChanged((isPressed) => {
|
||||
this.konva.transformer.rotationSnaps(isPressed ? [0, 45, 90, 135, 180, 225, 270, 315] : []);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the transformer's visual components to match the parent entity's position and bounding box.
|
||||
* @param position The position of the parent entity
|
||||
* @param bbox The bounding box of the parent entity
|
||||
*/
|
||||
update = (position: Coordinate, bbox: Rect) => {
|
||||
const onePixel = this.manager.getScaledPixel();
|
||||
const bboxPadding = this.manager.getScaledBboxPadding();
|
||||
|
||||
this.konva.bboxOutline.setAttrs({
|
||||
x: position.x + bbox.x - bboxPadding,
|
||||
y: position.y + bbox.y - bboxPadding,
|
||||
width: bbox.width + bboxPadding * 2,
|
||||
height: bbox.height + bboxPadding * 2,
|
||||
strokeWidth: onePixel,
|
||||
});
|
||||
this.konva.proxyRect.setAttrs({
|
||||
x: position.x + bbox.x,
|
||||
y: position.y + bbox.y,
|
||||
width: bbox.width,
|
||||
height: bbox.height,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the transformer's scale. This is called when the stage is scaled.
|
||||
*/
|
||||
scale = () => {
|
||||
const onePixel = this.manager.getScaledPixel();
|
||||
const bboxPadding = this.manager.getScaledBboxPadding();
|
||||
|
||||
this.konva.bboxOutline.setAttrs({
|
||||
x: this.konva.proxyRect.x() - bboxPadding,
|
||||
y: this.konva.proxyRect.y() - bboxPadding,
|
||||
width: this.konva.proxyRect.width() * this.konva.proxyRect.scaleX() + bboxPadding * 2,
|
||||
height: this.konva.proxyRect.height() * this.konva.proxyRect.scaleY() + bboxPadding * 2,
|
||||
strokeWidth: onePixel,
|
||||
});
|
||||
this.konva.transformer.forceUpdate();
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the transformer to a specific mode.
|
||||
* @param mode The mode to set the transformer to. The transformer can be in one of three modes:
|
||||
* - 'transform': The entity can be moved, resized, and rotated
|
||||
* - 'drag': The entity can only be moved
|
||||
* - 'off': The transformer is disabled
|
||||
*/
|
||||
setMode = (mode: 'transform' | 'drag' | 'off') => {
|
||||
this.mode = mode;
|
||||
if (mode === 'drag') {
|
||||
@ -321,11 +412,26 @@ export class CanvasTransformer {
|
||||
this.konva.bboxOutline.visible(false);
|
||||
};
|
||||
|
||||
getNodes = () => [this.konva.transformer, this.konva.proxyRect, this.konva.bboxOutline];
|
||||
|
||||
repr = () => {
|
||||
return {
|
||||
id: this.id,
|
||||
type: CanvasTransformer.TYPE,
|
||||
isActive: this.isTransformEnabled,
|
||||
mode: this.mode,
|
||||
isTransformEnabled: this.isTransformEnabled,
|
||||
isDragEnabled: this.isDragEnabled,
|
||||
};
|
||||
};
|
||||
|
||||
destroy = () => {
|
||||
this.log.trace('Destroying transformer');
|
||||
for (const { name, unsubscribe } of this.subscriptions) {
|
||||
this.log.trace({ name }, 'Cleaning up listener');
|
||||
unsubscribe();
|
||||
}
|
||||
this.konva.bboxOutline.destroy();
|
||||
this.konva.transformer.destroy();
|
||||
this.konva.proxyRect.destroy();
|
||||
};
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user