feat(ui): continue modularizing transform

This commit is contained in:
psychedelicious 2024-08-01 22:25:20 +10:00
parent abd22ba087
commit 243feecef9
2 changed files with 129 additions and 61 deletions

View File

@ -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);

View File

@ -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();
};
}