mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): transform cleanup
This commit is contained in:
parent
49733091c7
commit
ea02323095
@ -1,6 +1,6 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { Flex, Switch } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { BrushWidth } from 'features/controlLayers/components/BrushWidth';
|
||||
@ -14,6 +14,7 @@ import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoB
|
||||
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
||||
import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
export const ControlLayersToolbar = memo(() => {
|
||||
@ -27,12 +28,19 @@ export const ControlLayersToolbar = memo(() => {
|
||||
l.calculateBbox();
|
||||
}
|
||||
}, [canvasManager]);
|
||||
const debug = useCallback(() => {
|
||||
if (!canvasManager) {
|
||||
return;
|
||||
}
|
||||
canvasManager.logDebugInfo();
|
||||
}, [canvasManager]);
|
||||
const onChangeDebugging = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!canvasManager) {
|
||||
return;
|
||||
}
|
||||
if (e.target.checked) {
|
||||
canvasManager.enableDebugging();
|
||||
} else {
|
||||
canvasManager.disableDebugging();
|
||||
}
|
||||
},
|
||||
[canvasManager]
|
||||
);
|
||||
return (
|
||||
<Flex w="full" gap={2}>
|
||||
<Flex flex={1} justifyContent="center">
|
||||
@ -46,7 +54,7 @@ export const ControlLayersToolbar = memo(() => {
|
||||
{tool === 'eraser' && <EraserWidth />}
|
||||
</Flex>
|
||||
<Button onClick={bbox}>bbox</Button>
|
||||
<Button onClick={debug}>debug</Button>
|
||||
<Switch onChange={onChangeDebugging}>debug</Switch>
|
||||
<Flex flex={1} justifyContent="center">
|
||||
<Flex gap={2} marginInlineStart="auto" alignItems="center">
|
||||
<FillColorPicker />
|
||||
|
@ -25,7 +25,7 @@ export class CanvasBrushLine {
|
||||
this.id = id;
|
||||
|
||||
this.parent = parent;
|
||||
this.parent.log.trace(`Creating brush line ${this.id}`);
|
||||
this.parent._log.trace(`Creating brush line ${this.id}`);
|
||||
|
||||
this.konva = {
|
||||
group: new Konva.Group({
|
||||
@ -54,7 +54,7 @@ export class CanvasBrushLine {
|
||||
|
||||
async update(state: BrushLine, force?: boolean): Promise<boolean> {
|
||||
if (force || this.state !== state) {
|
||||
this.parent.log.trace(`Updating brush line ${this.id}`);
|
||||
this.parent._log.trace(`Updating brush line ${this.id}`);
|
||||
const { points, color, clip, strokeWidth } = state;
|
||||
this.konva.line.setAttrs({
|
||||
// A line with only one point will not be rendered, so we duplicate the points to make it visible
|
||||
@ -71,7 +71,7 @@ export class CanvasBrushLine {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.parent.log.trace(`Destroying brush line ${this.id}`);
|
||||
this.parent._log.trace(`Destroying brush line ${this.id}`);
|
||||
this.konva.group.destroy();
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ export class CanvasEraserLine {
|
||||
this.id = id;
|
||||
|
||||
this.parent = parent;
|
||||
this.parent.log.trace(`Creating eraser line ${this.id}`);
|
||||
this.parent._log.trace(`Creating eraser line ${this.id}`);
|
||||
|
||||
this.konva = {
|
||||
group: new Konva.Group({
|
||||
@ -55,7 +55,7 @@ export class CanvasEraserLine {
|
||||
|
||||
async update(state: EraserLine, force?: boolean): Promise<boolean> {
|
||||
if (force || this.state !== state) {
|
||||
this.parent.log.trace(`Updating eraser line ${this.id}`);
|
||||
this.parent._log.trace(`Updating eraser line ${this.id}`);
|
||||
const { points, clip, strokeWidth } = state;
|
||||
this.konva.line.setAttrs({
|
||||
// A line with only one point will not be rendered, so we duplicate the points to make it visible
|
||||
@ -71,7 +71,7 @@ export class CanvasEraserLine {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.parent.log.trace(`Destroying eraser line ${this.id}`);
|
||||
this.parent._log.trace(`Destroying eraser line ${this.id}`);
|
||||
this.konva.group.destroy();
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ export class CanvasImage {
|
||||
this.id = id;
|
||||
|
||||
this.parent = parent;
|
||||
this.parent.log.trace(`Creating image ${this.id}`);
|
||||
this.parent._log.trace(`Creating image ${this.id}`);
|
||||
|
||||
this.konva = {
|
||||
group: new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }),
|
||||
@ -77,13 +77,13 @@ export class CanvasImage {
|
||||
|
||||
async updateImageSource(imageName: string) {
|
||||
try {
|
||||
this.parent.log.trace(`Updating image source ${this.id}`);
|
||||
this.parent._log.trace(`Updating image source ${this.id}`);
|
||||
|
||||
this.isLoading = true;
|
||||
this.konva.group.visible(true);
|
||||
|
||||
if (!this.image) {
|
||||
this.konva.placeholder.group.visible(true);
|
||||
this.konva.placeholder.group.visible(false);
|
||||
this.konva.placeholder.text.text(t('common.loadingImage', 'Loading Image'));
|
||||
}
|
||||
|
||||
@ -130,7 +130,7 @@ export class CanvasImage {
|
||||
|
||||
async update(state: ImageObject, force?: boolean): Promise<boolean> {
|
||||
if (this.state !== state || force) {
|
||||
this.parent.log.trace(`Updating image ${this.id}`);
|
||||
this.parent._log.trace(`Updating image ${this.id}`);
|
||||
|
||||
const { width, height, x, y, image, filters } = state;
|
||||
if (this.state.image.name !== image.name || force) {
|
||||
@ -154,7 +154,7 @@ export class CanvasImage {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.parent.log.trace(`Destroying image ${this.id}`);
|
||||
this.parent._log.trace(`Destroying image ${this.id}`);
|
||||
this.konva.group.destroy();
|
||||
}
|
||||
}
|
||||
|
@ -24,24 +24,6 @@ import { uploadImage } from 'services/api/endpoints/images';
|
||||
import { assert } from 'tsafe';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const getCenter = (rect: Rect): Coordinate => {
|
||||
return {
|
||||
x: rect.x + rect.width / 2,
|
||||
y: rect.y + rect.height / 2,
|
||||
};
|
||||
};
|
||||
|
||||
window.getCenter = getCenter;
|
||||
|
||||
function rotatePoint(point: Coordinate, origin: Coordinate, deg: number): Coordinate {
|
||||
const angle = deg * (Math.PI / 180); // Convert to radians
|
||||
const rotatedX = Math.cos(angle) * (point.x - origin.x) - Math.sin(angle) * (point.y - origin.y) + origin.x;
|
||||
const rotatedY = Math.sin(angle) * (point.x - origin.x) + Math.cos(angle) * (point.y - origin.y) + origin.y;
|
||||
|
||||
return { x: rotatedX, y: rotatedY };
|
||||
}
|
||||
|
||||
window.rotatePoint = rotatePoint;
|
||||
export class CanvasLayer {
|
||||
static NAME_PREFIX = 'layer';
|
||||
static LAYER_NAME = `${CanvasLayer.NAME_PREFIX}_layer`;
|
||||
@ -51,8 +33,8 @@ export class CanvasLayer {
|
||||
static OBJECT_GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_object-group`;
|
||||
static BBOX_NAME = `${CanvasLayer.NAME_PREFIX}_bbox`;
|
||||
|
||||
private drawingBuffer: BrushLine | EraserLine | RectShape | null;
|
||||
private state: LayerEntity;
|
||||
_drawingBuffer: BrushLine | EraserLine | RectShape | null;
|
||||
_state: LayerEntity;
|
||||
|
||||
id: string;
|
||||
manager: CanvasManager;
|
||||
@ -66,10 +48,11 @@ export class CanvasLayer {
|
||||
};
|
||||
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage>;
|
||||
|
||||
log: Logger;
|
||||
bboxNeedsUpdate: boolean;
|
||||
_log: Logger;
|
||||
_bboxNeedsUpdate: boolean;
|
||||
_isFirstRender: boolean;
|
||||
|
||||
isTransforming: boolean;
|
||||
isFirstRender: boolean;
|
||||
|
||||
rect: Rect;
|
||||
bbox: Rect;
|
||||
@ -113,17 +96,7 @@ export class CanvasLayer {
|
||||
this.konva.layer.add(this.konva.bbox);
|
||||
|
||||
this.konva.transformer.on('transformstart', () => {
|
||||
console.log('>>> transformstart');
|
||||
console.log('interactionRect', {
|
||||
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(),
|
||||
});
|
||||
this.logBbox('transformstart bbox');
|
||||
console.log('this.state.position', this.state.position);
|
||||
this.logDebugInfo("'transformstart' fired");
|
||||
});
|
||||
|
||||
this.konva.transformer.on('transform', () => {
|
||||
@ -152,17 +125,7 @@ export class CanvasLayer {
|
||||
// this.konva.interactionRect.scaleY(scaleY);
|
||||
// this.konva.interactionRect.rotation(0);
|
||||
|
||||
console.log('>>> transform');
|
||||
console.log('activeAnchor', this.konva.transformer.getActiveAnchor());
|
||||
console.log('interactionRect', {
|
||||
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(),
|
||||
});
|
||||
this.logDebugInfo("'transform' fired");
|
||||
|
||||
this.konva.objectGroup.setAttrs({
|
||||
x: this.konva.interactionRect.x(),
|
||||
@ -171,33 +134,10 @@ export class CanvasLayer {
|
||||
scaleY: this.konva.interactionRect.scaleY(),
|
||||
rotation: this.konva.interactionRect.rotation(),
|
||||
});
|
||||
|
||||
console.log('objectGroup', {
|
||||
x: this.konva.objectGroup.x(),
|
||||
y: this.konva.objectGroup.y(),
|
||||
scaleX: this.konva.objectGroup.scaleX(),
|
||||
scaleY: this.konva.objectGroup.scaleY(),
|
||||
offsetX: this.konva.objectGroup.offsetX(),
|
||||
offsetY: this.konva.objectGroup.offsetY(),
|
||||
width: this.konva.objectGroup.width(),
|
||||
height: this.konva.objectGroup.height(),
|
||||
rotation: this.konva.objectGroup.rotation(),
|
||||
});
|
||||
});
|
||||
|
||||
this.konva.transformer.on('transformend', () => {
|
||||
// this.offsetX = this.konva.interactionRect.x() - this.state.position.x;
|
||||
// this.offsetY = this.konva.interactionRect.y() - this.state.position.y;
|
||||
// this.width = Math.round(this.konva.interactionRect.width() * this.konva.interactionRect.scaleX());
|
||||
// this.height = Math.round(this.konva.interactionRect.height() * this.konva.interactionRect.scaleY());
|
||||
// this.manager.stateApi.onPosChanged(
|
||||
// {
|
||||
// id: this.id,
|
||||
// position: { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() },
|
||||
// },
|
||||
// 'layer'
|
||||
// );
|
||||
this.logBbox('transformend bbox');
|
||||
this.logDebugInfo("'transformend' fired");
|
||||
});
|
||||
|
||||
this.konva.interactionRect.on('dragmove', () => {
|
||||
@ -220,7 +160,7 @@ export class CanvasLayer {
|
||||
});
|
||||
});
|
||||
this.konva.interactionRect.on('dragend', () => {
|
||||
this.logBbox('dragend bbox');
|
||||
this.logDebugInfo("'dragend' fired");
|
||||
|
||||
if (this.isTransforming) {
|
||||
// When the user cancels the transformation, we need to reset the layer, so we should not update the layer's
|
||||
@ -241,38 +181,38 @@ export class CanvasLayer {
|
||||
});
|
||||
|
||||
this.objects = new Map();
|
||||
this.drawingBuffer = null;
|
||||
this.state = state;
|
||||
this._drawingBuffer = null;
|
||||
this._state = state;
|
||||
this.rect = this.getDefaultRect();
|
||||
this.bbox = this.getDefaultRect();
|
||||
this.bboxNeedsUpdate = true;
|
||||
this._bboxNeedsUpdate = true;
|
||||
this.isTransforming = false;
|
||||
this.isFirstRender = true;
|
||||
this.log = this.manager.getLogger(`layer_${this.id}`);
|
||||
this._isFirstRender = true;
|
||||
this._log = this.manager.getLogger(`layer_${this.id}`);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.log.debug(`Layer ${this.id} - destroying`);
|
||||
this._log.debug(`Layer ${this.id} - destroying`);
|
||||
this.konva.layer.destroy();
|
||||
}
|
||||
|
||||
getDrawingBuffer() {
|
||||
return this.drawingBuffer;
|
||||
return this._drawingBuffer;
|
||||
}
|
||||
async setDrawingBuffer(obj: BrushLine | EraserLine | RectShape | null) {
|
||||
if (obj) {
|
||||
this.drawingBuffer = obj;
|
||||
await this._renderObject(this.drawingBuffer, true);
|
||||
this._drawingBuffer = obj;
|
||||
await this._renderObject(this._drawingBuffer, true);
|
||||
} else {
|
||||
this.drawingBuffer = null;
|
||||
this._drawingBuffer = null;
|
||||
}
|
||||
}
|
||||
|
||||
async finalizeDrawingBuffer() {
|
||||
if (!this.drawingBuffer) {
|
||||
if (!this._drawingBuffer) {
|
||||
return;
|
||||
}
|
||||
const drawingBuffer = this.drawingBuffer;
|
||||
const drawingBuffer = this._drawingBuffer;
|
||||
this.setDrawingBuffer(null);
|
||||
|
||||
// We need to give the objects a fresh ID else they will be considered the same object when they are re-rendered as
|
||||
@ -291,44 +231,50 @@ export class CanvasLayer {
|
||||
}
|
||||
|
||||
async update(arg?: { state: LayerEntity; toolState: CanvasV2State['tool']; isSelected: boolean }) {
|
||||
const state = get(arg, 'state', this.state);
|
||||
const state = get(arg, 'state', this._state);
|
||||
const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState());
|
||||
const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id));
|
||||
|
||||
if (!this.isFirstRender && state === this.state) {
|
||||
this.log.trace('State unchanged, skipping update');
|
||||
if (!this._isFirstRender && state === this._state) {
|
||||
this._log.trace('State unchanged, skipping update');
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.debug('Updating');
|
||||
this._log.debug('Updating');
|
||||
const { position, objects, opacity, isEnabled } = state;
|
||||
|
||||
if (this.isFirstRender || position !== this.state.position) {
|
||||
if (this._isFirstRender || position !== this._state.position) {
|
||||
await this.updatePosition({ position });
|
||||
}
|
||||
if (this.isFirstRender || objects !== this.state.objects) {
|
||||
if (this._isFirstRender || objects !== this._state.objects) {
|
||||
await this.updateObjects({ objects });
|
||||
}
|
||||
if (this.isFirstRender || opacity !== this.state.opacity) {
|
||||
if (this._isFirstRender || opacity !== this._state.opacity) {
|
||||
await this.updateOpacity({ opacity });
|
||||
}
|
||||
if (this.isFirstRender || isEnabled !== this.state.isEnabled) {
|
||||
if (this._isFirstRender || isEnabled !== this._state.isEnabled) {
|
||||
await this.updateVisibility({ isEnabled });
|
||||
}
|
||||
await this.updateInteraction({ toolState, isSelected });
|
||||
this.state = state;
|
||||
|
||||
if (this._isFirstRender) {
|
||||
await this.updateBbox();
|
||||
}
|
||||
|
||||
this._state = state;
|
||||
this._isFirstRender = false;
|
||||
}
|
||||
|
||||
async updateVisibility(arg?: { isEnabled: boolean }) {
|
||||
this.log.trace('Updating visibility');
|
||||
const isEnabled = get(arg, 'isEnabled', this.state.isEnabled);
|
||||
const hasObjects = this.objects.size > 0 || this.drawingBuffer !== null;
|
||||
this._log.trace('Updating visibility');
|
||||
const isEnabled = get(arg, 'isEnabled', this._state.isEnabled);
|
||||
const hasObjects = this.objects.size > 0 || this._drawingBuffer !== null;
|
||||
this.konva.layer.visible(isEnabled || hasObjects);
|
||||
}
|
||||
|
||||
async updatePosition(arg?: { position: Coordinate }) {
|
||||
this.log.trace('Updating position');
|
||||
const position = get(arg, 'position', this.state.position);
|
||||
this._log.trace('Updating position');
|
||||
const position = get(arg, 'position', this._state.position);
|
||||
const bboxPadding = this.manager.getScaledBboxPadding();
|
||||
|
||||
this.konva.objectGroup.setAttrs({
|
||||
@ -348,9 +294,9 @@ export class CanvasLayer {
|
||||
}
|
||||
|
||||
async updateObjects(arg?: { objects: LayerEntity['objects'] }) {
|
||||
this.log.trace('Updating objects');
|
||||
this._log.trace('Updating objects');
|
||||
|
||||
const objects = get(arg, 'objects', this.state.objects);
|
||||
const objects = get(arg, 'objects', this._state.objects);
|
||||
|
||||
const objectIds = objects.map(mapId);
|
||||
|
||||
@ -358,7 +304,7 @@ export class CanvasLayer {
|
||||
|
||||
// Destroy any objects that are no longer in state
|
||||
for (const object of this.objects.values()) {
|
||||
if (!objectIds.includes(object.id) && object.id !== this.drawingBuffer?.id) {
|
||||
if (!objectIds.includes(object.id) && object.id !== this._drawingBuffer?.id) {
|
||||
this.objects.delete(object.id);
|
||||
object.destroy();
|
||||
didUpdate = true;
|
||||
@ -371,8 +317,8 @@ export class CanvasLayer {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.drawingBuffer) {
|
||||
if (await this._renderObject(this.drawingBuffer)) {
|
||||
if (this._drawingBuffer) {
|
||||
if (await this._renderObject(this._drawingBuffer)) {
|
||||
didUpdate = true;
|
||||
}
|
||||
}
|
||||
@ -383,15 +329,15 @@ export class CanvasLayer {
|
||||
}
|
||||
|
||||
async updateOpacity(arg?: { opacity: number }) {
|
||||
this.log.trace('Updating opacity');
|
||||
this._log.trace('Updating opacity');
|
||||
|
||||
const opacity = get(arg, 'opacity', this.state.opacity);
|
||||
const opacity = get(arg, 'opacity', this._state.opacity);
|
||||
|
||||
this.konva.objectGroup.opacity(opacity);
|
||||
}
|
||||
|
||||
async updateInteraction(arg?: { toolState: CanvasV2State['tool']; isSelected: boolean }) {
|
||||
this.log.trace('Updating interaction');
|
||||
this._log.trace('Updating interaction');
|
||||
|
||||
const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState());
|
||||
const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id));
|
||||
@ -440,42 +386,49 @@ export class CanvasLayer {
|
||||
}
|
||||
|
||||
async updateBbox() {
|
||||
this.log.trace('Updating bbox');
|
||||
this._log.trace('Updating bbox');
|
||||
|
||||
// If the bbox has no width or height, that means the layer is fully transparent. This can happen if it is only
|
||||
// eraser lines, fully clipped brush lines or if it has been fully erased. In this case, we should reset the layer
|
||||
// so we aren't drawing shapes that do not render anything.
|
||||
// eraser lines, fully clipped brush lines or if it has been fully erased.
|
||||
if (this.bbox.width === 0 || this.bbox.height === 0) {
|
||||
this.manager.stateApi.onEntityReset({ id: this.id }, 'layer');
|
||||
if (this.objects.size > 0) {
|
||||
// The layer is fully transparent but has objects - reset it
|
||||
this.manager.stateApi.onEntityReset({ id: this.id }, 'layer');
|
||||
}
|
||||
this.konva.bbox.visible(false);
|
||||
this.konva.interactionRect.visible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.konva.bbox.visible(true);
|
||||
this.konva.interactionRect.visible(true);
|
||||
|
||||
const onePixel = this.manager.getScaledPixel();
|
||||
const bboxPadding = this.manager.getScaledBboxPadding();
|
||||
|
||||
this.konva.bbox.setAttrs({
|
||||
x: this.state.position.x + this.bbox.x - bboxPadding,
|
||||
y: this.state.position.y + this.bbox.y - bboxPadding,
|
||||
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.konva.interactionRect.setAttrs({
|
||||
x: this.state.position.x + this.bbox.x,
|
||||
y: this.state.position.y + this.bbox.y,
|
||||
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.konva.objectGroup.setAttrs({
|
||||
x: this.state.position.x + this.bbox.x,
|
||||
y: this.state.position.y + this.bbox.y,
|
||||
x: this._state.position.x + this.bbox.x,
|
||||
y: this._state.position.y + this.bbox.y,
|
||||
offsetX: this.bbox.x,
|
||||
offsetY: this.bbox.y,
|
||||
});
|
||||
}
|
||||
|
||||
async syncStageScale() {
|
||||
this.log.trace('Syncing scale to stage');
|
||||
this._log.trace('Syncing scale to stage');
|
||||
|
||||
const onePixel = this.manager.getScaledPixel();
|
||||
const bboxPadding = this.manager.getScaledBboxPadding();
|
||||
@ -552,7 +505,7 @@ export class CanvasLayer {
|
||||
}
|
||||
|
||||
async startTransform() {
|
||||
this.log.debug('Starting transform');
|
||||
this._log.debug('Starting transform');
|
||||
this.isTransforming = true;
|
||||
|
||||
// When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or
|
||||
@ -583,14 +536,15 @@ export class CanvasLayer {
|
||||
}
|
||||
|
||||
async applyTransform() {
|
||||
this.log.debug('Applying transform');
|
||||
this._log.debug('Applying transform');
|
||||
|
||||
this.isTransforming = false;
|
||||
const objectGroupClone = this.konva.objectGroup.clone();
|
||||
const interactionRectClone = this.konva.interactionRect.clone();
|
||||
const rect = interactionRectClone.getClientRect();
|
||||
const blob = await konvaNodeToBlob(objectGroupClone, rect);
|
||||
previewBlob(blob, 'transformed layer');
|
||||
if (this.manager._isDebugging) {
|
||||
previewBlob(blob, 'transformed layer');
|
||||
}
|
||||
const imageDTO = await uploadImage(blob, `${this.id}_transform.png`, 'other', true);
|
||||
const { dispatch } = getStore();
|
||||
dispatch(layerRasterized({ id: this.id, imageDTO, position: { x: rect.x, y: rect.y } }));
|
||||
@ -599,11 +553,11 @@ export class CanvasLayer {
|
||||
}
|
||||
|
||||
async cancelTransform() {
|
||||
this.log.debug('Canceling transform');
|
||||
this._log.debug('Canceling transform');
|
||||
|
||||
this.isTransforming = false;
|
||||
this.resetScale();
|
||||
await this.updatePosition({ position: this.state.position });
|
||||
await this.updatePosition({ position: this._state.position });
|
||||
await this.updateBbox();
|
||||
await this.updateInteraction({
|
||||
toolState: this.manager.stateApi.getToolState(),
|
||||
@ -616,7 +570,7 @@ export class CanvasLayer {
|
||||
}
|
||||
|
||||
calculateBbox = debounce(() => {
|
||||
this.log.debug('Calculating bbox');
|
||||
this._log.debug('Calculating bbox');
|
||||
|
||||
if (this.objects.size === 0) {
|
||||
this.rect = this.getDefaultRect();
|
||||
@ -625,7 +579,6 @@ export class CanvasLayer {
|
||||
return;
|
||||
}
|
||||
|
||||
let needsPixelBbox = false;
|
||||
const rect = this.konva.objectGroup.getClientRect({ skipTransform: true });
|
||||
|
||||
/**
|
||||
@ -640,6 +593,7 @@ export class CanvasLayer {
|
||||
* TODO(psyche): Using pixel data is slow. Is it possible to be clever and somehow subtract the eraser lines and
|
||||
* clipped areas from the client rect?
|
||||
*/
|
||||
let needsPixelBbox = false;
|
||||
for (const obj of this.objects.values()) {
|
||||
const isEraserLine = obj instanceof CanvasEraserLine;
|
||||
const isImage = obj instanceof CanvasImage;
|
||||
@ -653,7 +607,7 @@ export class CanvasLayer {
|
||||
if (!needsPixelBbox) {
|
||||
this.rect = deepClone(rect);
|
||||
this.bbox = deepClone(rect);
|
||||
this.log.trace({ bbox: this.bbox, rect: this.rect }, 'Got bbox from client rect');
|
||||
this._log.trace({ bbox: this.bbox, rect: this.rect }, 'Got bbox from client rect');
|
||||
this.updateBbox();
|
||||
return;
|
||||
}
|
||||
@ -682,19 +636,42 @@ export class CanvasLayer {
|
||||
} else {
|
||||
this.bbox = deepClone(rect);
|
||||
}
|
||||
this.log.trace({ bbox: this.bbox, rect: this.rect, extents }, `Got bbox from worker`);
|
||||
this._log.trace({ bbox: this.bbox, rect: this.rect, extents }, `Got bbox from worker`);
|
||||
this.updateBbox();
|
||||
clone.destroy();
|
||||
}
|
||||
);
|
||||
}, CanvasManager.BBOX_DEBOUNCE_MS);
|
||||
|
||||
logBbox(msg: string = 'bbox') {
|
||||
console.log(msg, {
|
||||
x: this.state.position.x,
|
||||
y: this.state.position.y,
|
||||
rect: deepClone(this.rect),
|
||||
bbox: deepClone(this.bbox),
|
||||
});
|
||||
logDebugInfo(msg = 'Debug info') {
|
||||
const debugInfo = {
|
||||
id: this.id,
|
||||
state: this._state,
|
||||
rect: this.rect,
|
||||
bbox: this.bbox,
|
||||
objects: Array.from(this.objects.values()).map((obj) => obj.id),
|
||||
isTransforming: this.isTransforming,
|
||||
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(),
|
||||
},
|
||||
objectGroupAttrs: {
|
||||
x: this.konva.objectGroup.x(),
|
||||
y: this.konva.objectGroup.y(),
|
||||
scaleX: this.konva.objectGroup.scaleX(),
|
||||
scaleY: this.konva.objectGroup.scaleY(),
|
||||
width: this.konva.objectGroup.width(),
|
||||
height: this.konva.objectGroup.height(),
|
||||
rotation: this.konva.objectGroup.rotation(),
|
||||
offsetX: this.konva.objectGroup.offsetX(),
|
||||
offsetY: this.konva.objectGroup.offsetY(),
|
||||
},
|
||||
};
|
||||
this._log.debug(debugInfo, msg);
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ type Util = {
|
||||
export const $canvasManager = atom<CanvasManager | null>(null);
|
||||
|
||||
export class CanvasManager {
|
||||
private static BBOX_PADDING_PX = 5;
|
||||
static BBOX_PADDING_PX = 5;
|
||||
static BBOX_DEBOUNCE_MS = 300;
|
||||
|
||||
stage: Konva.Stage;
|
||||
@ -82,13 +82,15 @@ export class CanvasManager {
|
||||
log: Logger;
|
||||
workerLog: Logger;
|
||||
|
||||
_isDebugging: boolean;
|
||||
|
||||
onTransform: ((isTransforming: boolean) => void) | null;
|
||||
|
||||
private store: Store<RootState>;
|
||||
private isFirstRender: boolean;
|
||||
private prevState: CanvasV2State;
|
||||
private worker: Worker;
|
||||
private tasks: Map<string, { task: GetBboxTask; onComplete: (extents: Extents | null) => void }>;
|
||||
_store: Store<RootState>;
|
||||
_isFirstRender: boolean;
|
||||
_prevState: CanvasV2State;
|
||||
_worker: Worker;
|
||||
_tasks: Map<string, { task: GetBboxTask; onComplete: (extents: Extents | null) => void }>;
|
||||
|
||||
constructor(
|
||||
stage: Konva.Stage,
|
||||
@ -99,10 +101,10 @@ export class CanvasManager {
|
||||
) {
|
||||
this.stage = stage;
|
||||
this.container = container;
|
||||
this.store = store;
|
||||
this.stateApi = new CanvasStateApi(this.store);
|
||||
this.prevState = this.stateApi.getState();
|
||||
this.isFirstRender = true;
|
||||
this._store = store;
|
||||
this.stateApi = new CanvasStateApi(this._store);
|
||||
this._prevState = this.stateApi.getState();
|
||||
this._isFirstRender = true;
|
||||
|
||||
this.log = logger('canvas');
|
||||
this.workerLog = logger('worker');
|
||||
@ -133,9 +135,9 @@ export class CanvasManager {
|
||||
this.initialImage = new CanvasInitialImage(this.stateApi.getInitialImageState(), this);
|
||||
this.stage.add(this.initialImage.konva.layer);
|
||||
|
||||
this.worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module', name: 'worker' });
|
||||
this.tasks = new Map();
|
||||
this.worker.onmessage = (event: MessageEvent<ExtentsResult | WorkerLogMessage>) => {
|
||||
this._worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module', name: 'worker' });
|
||||
this._tasks = new Map();
|
||||
this._worker.onmessage = (event: MessageEvent<ExtentsResult | WorkerLogMessage>) => {
|
||||
const { type, data } = event.data;
|
||||
if (type === 'log') {
|
||||
if (data.ctx) {
|
||||
@ -144,20 +146,30 @@ export class CanvasManager {
|
||||
this.workerLog[data.level](data.message);
|
||||
}
|
||||
} else if (type === 'extents') {
|
||||
const task = this.tasks.get(data.id);
|
||||
const task = this._tasks.get(data.id);
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
task.onComplete(data.extents);
|
||||
}
|
||||
};
|
||||
this.worker.onerror = (event) => {
|
||||
this._worker.onerror = (event) => {
|
||||
this.log.error({ message: event.message }, 'Worker error');
|
||||
};
|
||||
this.worker.onmessageerror = () => {
|
||||
this._worker.onmessageerror = () => {
|
||||
this.log.error('Worker message error');
|
||||
};
|
||||
this.onTransform = null;
|
||||
this._isDebugging = false;
|
||||
}
|
||||
|
||||
enableDebugging() {
|
||||
this._isDebugging = true;
|
||||
this.logDebugInfo();
|
||||
}
|
||||
|
||||
disableDebugging() {
|
||||
this._isDebugging = false;
|
||||
}
|
||||
|
||||
getLogger(namespace: string) {
|
||||
@ -171,8 +183,8 @@ export class CanvasManager {
|
||||
type: 'get_bbox',
|
||||
data: { ...data, id },
|
||||
};
|
||||
this.tasks.set(id, { task, onComplete });
|
||||
this.worker.postMessage(task, [data.buffer]);
|
||||
this._tasks.set(id, { task, onComplete });
|
||||
this._worker.postMessage(task, [data.buffer]);
|
||||
}
|
||||
|
||||
async renderInitialImage() {
|
||||
@ -306,12 +318,12 @@ export class CanvasManager {
|
||||
render = async () => {
|
||||
const state = this.stateApi.getState();
|
||||
|
||||
if (this.prevState === state && !this.isFirstRender) {
|
||||
if (this._prevState === state && !this._isFirstRender) {
|
||||
this.log.trace('No changes detected, skipping render');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isFirstRender || state.layers.entities !== this.prevState.layers.entities) {
|
||||
if (this._isFirstRender || state.layers.entities !== this._prevState.layers.entities) {
|
||||
this.log.debug('Rendering layers');
|
||||
|
||||
for (const canvasLayer of this.layers.values()) {
|
||||
@ -339,9 +351,9 @@ export class CanvasManager {
|
||||
}
|
||||
|
||||
if (
|
||||
this.isFirstRender ||
|
||||
state.tool.selected !== this.prevState.tool.selected ||
|
||||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||
this._isFirstRender ||
|
||||
state.tool.selected !== this._prevState.tool.selected ||
|
||||
state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
|
||||
) {
|
||||
this.log.debug('Updating interaction');
|
||||
for (const layer of this.layers.values()) {
|
||||
@ -350,89 +362,89 @@ export class CanvasManager {
|
||||
}
|
||||
|
||||
if (
|
||||
this.isFirstRender ||
|
||||
state.initialImage !== this.prevState.initialImage ||
|
||||
state.bbox.rect !== this.prevState.bbox.rect ||
|
||||
state.tool.selected !== this.prevState.tool.selected ||
|
||||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||
this._isFirstRender ||
|
||||
state.initialImage !== this._prevState.initialImage ||
|
||||
state.bbox.rect !== this._prevState.bbox.rect ||
|
||||
state.tool.selected !== this._prevState.tool.selected ||
|
||||
state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
|
||||
) {
|
||||
this.log.debug('Rendering initial image');
|
||||
await this.renderInitialImage();
|
||||
}
|
||||
|
||||
if (
|
||||
this.isFirstRender ||
|
||||
state.regions.entities !== this.prevState.regions.entities ||
|
||||
state.settings.maskOpacity !== this.prevState.settings.maskOpacity ||
|
||||
state.tool.selected !== this.prevState.tool.selected ||
|
||||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||
this._isFirstRender ||
|
||||
state.regions.entities !== this._prevState.regions.entities ||
|
||||
state.settings.maskOpacity !== this._prevState.settings.maskOpacity ||
|
||||
state.tool.selected !== this._prevState.tool.selected ||
|
||||
state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
|
||||
) {
|
||||
this.log.debug('Rendering regions');
|
||||
await this.renderRegions();
|
||||
}
|
||||
|
||||
if (
|
||||
this.isFirstRender ||
|
||||
state.inpaintMask !== this.prevState.inpaintMask ||
|
||||
state.settings.maskOpacity !== this.prevState.settings.maskOpacity ||
|
||||
state.tool.selected !== this.prevState.tool.selected ||
|
||||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||
this._isFirstRender ||
|
||||
state.inpaintMask !== this._prevState.inpaintMask ||
|
||||
state.settings.maskOpacity !== this._prevState.settings.maskOpacity ||
|
||||
state.tool.selected !== this._prevState.tool.selected ||
|
||||
state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
|
||||
) {
|
||||
this.log.debug('Rendering inpaint mask');
|
||||
await this.renderInpaintMask();
|
||||
}
|
||||
|
||||
if (
|
||||
this.isFirstRender ||
|
||||
state.controlAdapters.entities !== this.prevState.controlAdapters.entities ||
|
||||
state.tool.selected !== this.prevState.tool.selected ||
|
||||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||
this._isFirstRender ||
|
||||
state.controlAdapters.entities !== this._prevState.controlAdapters.entities ||
|
||||
state.tool.selected !== this._prevState.tool.selected ||
|
||||
state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
|
||||
) {
|
||||
this.log.debug('Rendering control adapters');
|
||||
await this.renderControlAdapters();
|
||||
}
|
||||
|
||||
if (
|
||||
this.isFirstRender ||
|
||||
state.bbox !== this.prevState.bbox ||
|
||||
state.tool.selected !== this.prevState.tool.selected ||
|
||||
state.session.isActive !== this.prevState.session.isActive
|
||||
this._isFirstRender ||
|
||||
state.bbox !== this._prevState.bbox ||
|
||||
state.tool.selected !== this._prevState.tool.selected ||
|
||||
state.session.isActive !== this._prevState.session.isActive
|
||||
) {
|
||||
this.log.debug('Rendering generation bbox');
|
||||
await this.preview.bbox.render();
|
||||
}
|
||||
|
||||
if (
|
||||
this.isFirstRender ||
|
||||
state.layers !== this.prevState.layers ||
|
||||
state.controlAdapters !== this.prevState.controlAdapters ||
|
||||
state.regions !== this.prevState.regions
|
||||
this._isFirstRender ||
|
||||
state.layers !== this._prevState.layers ||
|
||||
state.controlAdapters !== this._prevState.controlAdapters ||
|
||||
state.regions !== this._prevState.regions
|
||||
) {
|
||||
// this.log.debug('Updating entity bboxes');
|
||||
// debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged);
|
||||
}
|
||||
|
||||
if (this.isFirstRender || state.session !== this.prevState.session) {
|
||||
if (this._isFirstRender || state.session !== this._prevState.session) {
|
||||
this.log.debug('Rendering staging area');
|
||||
await this.preview.stagingArea.render();
|
||||
}
|
||||
|
||||
if (
|
||||
this.isFirstRender ||
|
||||
state.layers.entities !== this.prevState.layers.entities ||
|
||||
state.controlAdapters.entities !== this.prevState.controlAdapters.entities ||
|
||||
state.regions.entities !== this.prevState.regions.entities ||
|
||||
state.inpaintMask !== this.prevState.inpaintMask ||
|
||||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||
this._isFirstRender ||
|
||||
state.layers.entities !== this._prevState.layers.entities ||
|
||||
state.controlAdapters.entities !== this._prevState.controlAdapters.entities ||
|
||||
state.regions.entities !== this._prevState.regions.entities ||
|
||||
state.inpaintMask !== this._prevState.inpaintMask ||
|
||||
state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
|
||||
) {
|
||||
this.log.debug('Arranging entities');
|
||||
await this.arrangeEntities();
|
||||
}
|
||||
|
||||
this.prevState = state;
|
||||
this._prevState = state;
|
||||
|
||||
if (this.isFirstRender) {
|
||||
this.isFirstRender = false;
|
||||
if (this._isFirstRender) {
|
||||
this._isFirstRender = false;
|
||||
}
|
||||
};
|
||||
|
||||
@ -448,7 +460,7 @@ export class CanvasManager {
|
||||
resizeObserver.observe(this.container);
|
||||
this.fitStageToContainer();
|
||||
|
||||
const unsubscribeRenderer = this.store.subscribe(this.render);
|
||||
const unsubscribeRenderer = this._store.subscribe(this.render);
|
||||
|
||||
// When we this flag, we need to render the staging area
|
||||
$shouldShowStagedImage.subscribe(async (shouldShowStagedImage, prevShouldShowStagedImage) => {
|
||||
|
@ -26,7 +26,7 @@ export class CanvasRect {
|
||||
this.id = id;
|
||||
|
||||
this.parent = parent;
|
||||
this.parent.log.trace(`Creating rect ${this.id}`);
|
||||
this.parent._log.trace(`Creating rect ${this.id}`);
|
||||
|
||||
this.konva = {
|
||||
group: new Konva.Group({ name: CanvasRect.GROUP_NAME, listening: false }),
|
||||
@ -47,7 +47,7 @@ export class CanvasRect {
|
||||
|
||||
async update(state: RectShape, force?: boolean): Promise<boolean> {
|
||||
if (this.state !== state || force) {
|
||||
this.parent.log.trace(`Updating rect ${this.id}`);
|
||||
this.parent._log.trace(`Updating rect ${this.id}`);
|
||||
const { x, y, width, height, color } = state;
|
||||
this.konva.rect.setAttrs({
|
||||
x,
|
||||
@ -64,7 +64,7 @@ export class CanvasRect {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.parent.log.trace(`Destroying rect ${this.id}`);
|
||||
this.parent._log.trace(`Destroying rect ${this.id}`);
|
||||
this.konva.group.destroy();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user