feat(ui): transform cleanup

This commit is contained in:
psychedelicious 2024-07-30 20:03:15 +10:00
parent 49733091c7
commit ea02323095
7 changed files with 212 additions and 215 deletions

View File

@ -1,6 +1,6 @@
/* eslint-disable i18next/no-literal-string */ /* eslint-disable i18next/no-literal-string */
import { Button } from '@chakra-ui/react'; 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 { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { BrushWidth } from 'features/controlLayers/components/BrushWidth'; 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 { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu'; import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
export const ControlLayersToolbar = memo(() => { export const ControlLayersToolbar = memo(() => {
@ -27,12 +28,19 @@ export const ControlLayersToolbar = memo(() => {
l.calculateBbox(); l.calculateBbox();
} }
}, [canvasManager]); }, [canvasManager]);
const debug = useCallback(() => { const onChangeDebugging = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
if (!canvasManager) { if (!canvasManager) {
return; return;
} }
canvasManager.logDebugInfo(); if (e.target.checked) {
}, [canvasManager]); canvasManager.enableDebugging();
} else {
canvasManager.disableDebugging();
}
},
[canvasManager]
);
return ( return (
<Flex w="full" gap={2}> <Flex w="full" gap={2}>
<Flex flex={1} justifyContent="center"> <Flex flex={1} justifyContent="center">
@ -46,7 +54,7 @@ export const ControlLayersToolbar = memo(() => {
{tool === 'eraser' && <EraserWidth />} {tool === 'eraser' && <EraserWidth />}
</Flex> </Flex>
<Button onClick={bbox}>bbox</Button> <Button onClick={bbox}>bbox</Button>
<Button onClick={debug}>debug</Button> <Switch onChange={onChangeDebugging}>debug</Switch>
<Flex flex={1} justifyContent="center"> <Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineStart="auto" alignItems="center"> <Flex gap={2} marginInlineStart="auto" alignItems="center">
<FillColorPicker /> <FillColorPicker />

View File

@ -25,7 +25,7 @@ export class CanvasBrushLine {
this.id = id; this.id = id;
this.parent = parent; this.parent = parent;
this.parent.log.trace(`Creating brush line ${this.id}`); this.parent._log.trace(`Creating brush line ${this.id}`);
this.konva = { this.konva = {
group: new Konva.Group({ group: new Konva.Group({
@ -54,7 +54,7 @@ export class CanvasBrushLine {
async update(state: BrushLine, force?: boolean): Promise<boolean> { async update(state: BrushLine, force?: boolean): Promise<boolean> {
if (force || this.state !== state) { 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; const { points, color, clip, strokeWidth } = state;
this.konva.line.setAttrs({ this.konva.line.setAttrs({
// A line with only one point will not be rendered, so we duplicate the points to make it visible // 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() { destroy() {
this.parent.log.trace(`Destroying brush line ${this.id}`); this.parent._log.trace(`Destroying brush line ${this.id}`);
this.konva.group.destroy(); this.konva.group.destroy();
} }
} }

View File

@ -26,7 +26,7 @@ export class CanvasEraserLine {
this.id = id; this.id = id;
this.parent = parent; this.parent = parent;
this.parent.log.trace(`Creating eraser line ${this.id}`); this.parent._log.trace(`Creating eraser line ${this.id}`);
this.konva = { this.konva = {
group: new Konva.Group({ group: new Konva.Group({
@ -55,7 +55,7 @@ export class CanvasEraserLine {
async update(state: EraserLine, force?: boolean): Promise<boolean> { async update(state: EraserLine, force?: boolean): Promise<boolean> {
if (force || this.state !== state) { 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; const { points, clip, strokeWidth } = state;
this.konva.line.setAttrs({ this.konva.line.setAttrs({
// A line with only one point will not be rendered, so we duplicate the points to make it visible // 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() { destroy() {
this.parent.log.trace(`Destroying eraser line ${this.id}`); this.parent._log.trace(`Destroying eraser line ${this.id}`);
this.konva.group.destroy(); this.konva.group.destroy();
} }
} }

View File

@ -36,7 +36,7 @@ export class CanvasImage {
this.id = id; this.id = id;
this.parent = parent; this.parent = parent;
this.parent.log.trace(`Creating image ${this.id}`); this.parent._log.trace(`Creating image ${this.id}`);
this.konva = { this.konva = {
group: new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }), group: new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }),
@ -77,13 +77,13 @@ export class CanvasImage {
async updateImageSource(imageName: string) { async updateImageSource(imageName: string) {
try { try {
this.parent.log.trace(`Updating image source ${this.id}`); this.parent._log.trace(`Updating image source ${this.id}`);
this.isLoading = true; this.isLoading = true;
this.konva.group.visible(true); this.konva.group.visible(true);
if (!this.image) { 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')); 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> { async update(state: ImageObject, force?: boolean): Promise<boolean> {
if (this.state !== state || force) { 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; const { width, height, x, y, image, filters } = state;
if (this.state.image.name !== image.name || force) { if (this.state.image.name !== image.name || force) {
@ -154,7 +154,7 @@ export class CanvasImage {
} }
destroy() { destroy() {
this.parent.log.trace(`Destroying image ${this.id}`); this.parent._log.trace(`Destroying image ${this.id}`);
this.konva.group.destroy(); this.konva.group.destroy();
} }
} }

View File

@ -24,24 +24,6 @@ import { uploadImage } from 'services/api/endpoints/images';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid'; 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 { export class CanvasLayer {
static NAME_PREFIX = 'layer'; static NAME_PREFIX = 'layer';
static LAYER_NAME = `${CanvasLayer.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 OBJECT_GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_object-group`;
static BBOX_NAME = `${CanvasLayer.NAME_PREFIX}_bbox`; static BBOX_NAME = `${CanvasLayer.NAME_PREFIX}_bbox`;
private drawingBuffer: BrushLine | EraserLine | RectShape | null; _drawingBuffer: BrushLine | EraserLine | RectShape | null;
private state: LayerEntity; _state: LayerEntity;
id: string; id: string;
manager: CanvasManager; manager: CanvasManager;
@ -66,10 +48,11 @@ export class CanvasLayer {
}; };
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage>; objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage>;
log: Logger; _log: Logger;
bboxNeedsUpdate: boolean; _bboxNeedsUpdate: boolean;
_isFirstRender: boolean;
isTransforming: boolean; isTransforming: boolean;
isFirstRender: boolean;
rect: Rect; rect: Rect;
bbox: Rect; bbox: Rect;
@ -113,17 +96,7 @@ export class CanvasLayer {
this.konva.layer.add(this.konva.bbox); this.konva.layer.add(this.konva.bbox);
this.konva.transformer.on('transformstart', () => { this.konva.transformer.on('transformstart', () => {
console.log('>>> transformstart'); this.logDebugInfo("'transformstart' fired");
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.konva.transformer.on('transform', () => { this.konva.transformer.on('transform', () => {
@ -152,17 +125,7 @@ export class CanvasLayer {
// this.konva.interactionRect.scaleY(scaleY); // this.konva.interactionRect.scaleY(scaleY);
// this.konva.interactionRect.rotation(0); // this.konva.interactionRect.rotation(0);
console.log('>>> transform'); this.logDebugInfo("'transform' fired");
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.konva.objectGroup.setAttrs({ this.konva.objectGroup.setAttrs({
x: this.konva.interactionRect.x(), x: this.konva.interactionRect.x(),
@ -171,33 +134,10 @@ export class CanvasLayer {
scaleY: this.konva.interactionRect.scaleY(), scaleY: this.konva.interactionRect.scaleY(),
rotation: this.konva.interactionRect.rotation(), 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.konva.transformer.on('transformend', () => {
// this.offsetX = this.konva.interactionRect.x() - this.state.position.x; this.logDebugInfo("'transformend' fired");
// 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.konva.interactionRect.on('dragmove', () => { this.konva.interactionRect.on('dragmove', () => {
@ -220,7 +160,7 @@ export class CanvasLayer {
}); });
}); });
this.konva.interactionRect.on('dragend', () => { this.konva.interactionRect.on('dragend', () => {
this.logBbox('dragend bbox'); this.logDebugInfo("'dragend' fired");
if (this.isTransforming) { if (this.isTransforming) {
// When the user cancels the transformation, we need to reset the layer, so we should not update the layer's // 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.objects = new Map();
this.drawingBuffer = null; this._drawingBuffer = null;
this.state = state; this._state = state;
this.rect = this.getDefaultRect(); this.rect = this.getDefaultRect();
this.bbox = this.getDefaultRect(); this.bbox = this.getDefaultRect();
this.bboxNeedsUpdate = true; this._bboxNeedsUpdate = true;
this.isTransforming = false; this.isTransforming = false;
this.isFirstRender = true; this._isFirstRender = true;
this.log = this.manager.getLogger(`layer_${this.id}`); this._log = this.manager.getLogger(`layer_${this.id}`);
} }
destroy(): void { destroy(): void {
this.log.debug(`Layer ${this.id} - destroying`); this._log.debug(`Layer ${this.id} - destroying`);
this.konva.layer.destroy(); this.konva.layer.destroy();
} }
getDrawingBuffer() { getDrawingBuffer() {
return this.drawingBuffer; return this._drawingBuffer;
} }
async setDrawingBuffer(obj: BrushLine | EraserLine | RectShape | null) { async setDrawingBuffer(obj: BrushLine | EraserLine | RectShape | null) {
if (obj) { if (obj) {
this.drawingBuffer = obj; this._drawingBuffer = obj;
await this._renderObject(this.drawingBuffer, true); await this._renderObject(this._drawingBuffer, true);
} else { } else {
this.drawingBuffer = null; this._drawingBuffer = null;
} }
} }
async finalizeDrawingBuffer() { async finalizeDrawingBuffer() {
if (!this.drawingBuffer) { if (!this._drawingBuffer) {
return; return;
} }
const drawingBuffer = this.drawingBuffer; const drawingBuffer = this._drawingBuffer;
this.setDrawingBuffer(null); 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 // 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 }) { 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 toolState = get(arg, 'toolState', this.manager.stateApi.getToolState());
const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id)); const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id));
if (!this.isFirstRender && state === this.state) { if (!this._isFirstRender && state === this._state) {
this.log.trace('State unchanged, skipping update'); this._log.trace('State unchanged, skipping update');
return; return;
} }
this.log.debug('Updating'); this._log.debug('Updating');
const { position, objects, opacity, isEnabled } = state; const { position, objects, opacity, isEnabled } = state;
if (this.isFirstRender || position !== this.state.position) { if (this._isFirstRender || position !== this._state.position) {
await this.updatePosition({ position }); await this.updatePosition({ position });
} }
if (this.isFirstRender || objects !== this.state.objects) { if (this._isFirstRender || objects !== this._state.objects) {
await this.updateObjects({ objects }); await this.updateObjects({ objects });
} }
if (this.isFirstRender || opacity !== this.state.opacity) { if (this._isFirstRender || opacity !== this._state.opacity) {
await this.updateOpacity({ 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.updateVisibility({ isEnabled });
} }
await this.updateInteraction({ toolState, isSelected }); 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 }) { async updateVisibility(arg?: { isEnabled: boolean }) {
this.log.trace('Updating visibility'); this._log.trace('Updating visibility');
const isEnabled = get(arg, 'isEnabled', this.state.isEnabled); const isEnabled = get(arg, 'isEnabled', this._state.isEnabled);
const hasObjects = this.objects.size > 0 || this.drawingBuffer !== null; const hasObjects = this.objects.size > 0 || this._drawingBuffer !== null;
this.konva.layer.visible(isEnabled || hasObjects); this.konva.layer.visible(isEnabled || hasObjects);
} }
async updatePosition(arg?: { position: Coordinate }) { async updatePosition(arg?: { position: Coordinate }) {
this.log.trace('Updating position'); this._log.trace('Updating position');
const position = get(arg, 'position', this.state.position); const position = get(arg, 'position', this._state.position);
const bboxPadding = this.manager.getScaledBboxPadding(); const bboxPadding = this.manager.getScaledBboxPadding();
this.konva.objectGroup.setAttrs({ this.konva.objectGroup.setAttrs({
@ -348,9 +294,9 @@ export class CanvasLayer {
} }
async updateObjects(arg?: { objects: LayerEntity['objects'] }) { 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); const objectIds = objects.map(mapId);
@ -358,7 +304,7 @@ export class CanvasLayer {
// Destroy any objects that are no longer in state // Destroy any objects that are no longer in state
for (const object of this.objects.values()) { 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); this.objects.delete(object.id);
object.destroy(); object.destroy();
didUpdate = true; didUpdate = true;
@ -371,8 +317,8 @@ export class CanvasLayer {
} }
} }
if (this.drawingBuffer) { if (this._drawingBuffer) {
if (await this._renderObject(this.drawingBuffer)) { if (await this._renderObject(this._drawingBuffer)) {
didUpdate = true; didUpdate = true;
} }
} }
@ -383,15 +329,15 @@ export class CanvasLayer {
} }
async updateOpacity(arg?: { opacity: number }) { 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); this.konva.objectGroup.opacity(opacity);
} }
async updateInteraction(arg?: { toolState: CanvasV2State['tool']; isSelected: boolean }) { 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 toolState = get(arg, 'toolState', this.manager.stateApi.getToolState());
const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id)); const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id));
@ -440,42 +386,49 @@ export class CanvasLayer {
} }
async updateBbox() { 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 // 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 // eraser lines, fully clipped brush lines or if it has been fully erased.
// so we aren't drawing shapes that do not render anything.
if (this.bbox.width === 0 || this.bbox.height === 0) { if (this.bbox.width === 0 || this.bbox.height === 0) {
if (this.objects.size > 0) {
// The layer is fully transparent but has objects - reset it
this.manager.stateApi.onEntityReset({ id: this.id }, 'layer'); this.manager.stateApi.onEntityReset({ id: this.id }, 'layer');
}
this.konva.bbox.visible(false);
this.konva.interactionRect.visible(false);
return; return;
} }
this.konva.bbox.visible(true);
this.konva.interactionRect.visible(true);
const onePixel = this.manager.getScaledPixel(); const onePixel = this.manager.getScaledPixel();
const bboxPadding = this.manager.getScaledBboxPadding(); const bboxPadding = this.manager.getScaledBboxPadding();
this.konva.bbox.setAttrs({ this.konva.bbox.setAttrs({
x: this.state.position.x + this.bbox.x - bboxPadding, x: this._state.position.x + this.bbox.x - bboxPadding,
y: this.state.position.y + this.bbox.y - bboxPadding, y: this._state.position.y + this.bbox.y - bboxPadding,
width: this.bbox.width + bboxPadding * 2, width: this.bbox.width + bboxPadding * 2,
height: this.bbox.height + bboxPadding * 2, height: this.bbox.height + bboxPadding * 2,
strokeWidth: onePixel, strokeWidth: onePixel,
}); });
this.konva.interactionRect.setAttrs({ this.konva.interactionRect.setAttrs({
x: this.state.position.x + this.bbox.x, x: this._state.position.x + this.bbox.x,
y: this.state.position.y + this.bbox.y, y: this._state.position.y + this.bbox.y,
width: this.bbox.width, width: this.bbox.width,
height: this.bbox.height, height: this.bbox.height,
}); });
this.konva.objectGroup.setAttrs({ this.konva.objectGroup.setAttrs({
x: this.state.position.x + this.bbox.x, x: this._state.position.x + this.bbox.x,
y: this.state.position.y + this.bbox.y, y: this._state.position.y + this.bbox.y,
offsetX: this.bbox.x, offsetX: this.bbox.x,
offsetY: this.bbox.y, offsetY: this.bbox.y,
}); });
} }
async syncStageScale() { async syncStageScale() {
this.log.trace('Syncing scale to stage'); this._log.trace('Syncing scale to stage');
const onePixel = this.manager.getScaledPixel(); const onePixel = this.manager.getScaledPixel();
const bboxPadding = this.manager.getScaledBboxPadding(); const bboxPadding = this.manager.getScaledBboxPadding();
@ -552,7 +505,7 @@ export class CanvasLayer {
} }
async startTransform() { async startTransform() {
this.log.debug('Starting transform'); this._log.debug('Starting transform');
this.isTransforming = true; this.isTransforming = true;
// When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or // 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() { async applyTransform() {
this.log.debug('Applying transform'); this._log.debug('Applying transform');
this.isTransforming = false;
const objectGroupClone = this.konva.objectGroup.clone(); const objectGroupClone = this.konva.objectGroup.clone();
const interactionRectClone = this.konva.interactionRect.clone(); const interactionRectClone = this.konva.interactionRect.clone();
const rect = interactionRectClone.getClientRect(); const rect = interactionRectClone.getClientRect();
const blob = await konvaNodeToBlob(objectGroupClone, rect); const blob = await konvaNodeToBlob(objectGroupClone, rect);
if (this.manager._isDebugging) {
previewBlob(blob, 'transformed layer'); previewBlob(blob, 'transformed layer');
}
const imageDTO = await uploadImage(blob, `${this.id}_transform.png`, 'other', true); const imageDTO = await uploadImage(blob, `${this.id}_transform.png`, 'other', true);
const { dispatch } = getStore(); const { dispatch } = getStore();
dispatch(layerRasterized({ id: this.id, imageDTO, position: { x: rect.x, y: rect.y } })); dispatch(layerRasterized({ id: this.id, imageDTO, position: { x: rect.x, y: rect.y } }));
@ -599,11 +553,11 @@ export class CanvasLayer {
} }
async cancelTransform() { async cancelTransform() {
this.log.debug('Canceling transform'); this._log.debug('Canceling transform');
this.isTransforming = false; this.isTransforming = false;
this.resetScale(); this.resetScale();
await this.updatePosition({ position: this.state.position }); await this.updatePosition({ position: this._state.position });
await this.updateBbox(); await this.updateBbox();
await this.updateInteraction({ await this.updateInteraction({
toolState: this.manager.stateApi.getToolState(), toolState: this.manager.stateApi.getToolState(),
@ -616,7 +570,7 @@ export class CanvasLayer {
} }
calculateBbox = debounce(() => { calculateBbox = debounce(() => {
this.log.debug('Calculating bbox'); this._log.debug('Calculating bbox');
if (this.objects.size === 0) { if (this.objects.size === 0) {
this.rect = this.getDefaultRect(); this.rect = this.getDefaultRect();
@ -625,7 +579,6 @@ export class CanvasLayer {
return; return;
} }
let needsPixelBbox = false;
const rect = this.konva.objectGroup.getClientRect({ skipTransform: true }); 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 * 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? * clipped areas from the client rect?
*/ */
let needsPixelBbox = false;
for (const obj of this.objects.values()) { for (const obj of this.objects.values()) {
const isEraserLine = obj instanceof CanvasEraserLine; const isEraserLine = obj instanceof CanvasEraserLine;
const isImage = obj instanceof CanvasImage; const isImage = obj instanceof CanvasImage;
@ -653,7 +607,7 @@ export class CanvasLayer {
if (!needsPixelBbox) { if (!needsPixelBbox) {
this.rect = deepClone(rect); this.rect = deepClone(rect);
this.bbox = 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(); this.updateBbox();
return; return;
} }
@ -682,19 +636,42 @@ export class CanvasLayer {
} else { } else {
this.bbox = deepClone(rect); 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(); this.updateBbox();
clone.destroy(); clone.destroy();
} }
); );
}, CanvasManager.BBOX_DEBOUNCE_MS); }, CanvasManager.BBOX_DEBOUNCE_MS);
logBbox(msg: string = 'bbox') { logDebugInfo(msg = 'Debug info') {
console.log(msg, { const debugInfo = {
x: this.state.position.x, id: this.id,
y: this.state.position.y, state: this._state,
rect: deepClone(this.rect), rect: this.rect,
bbox: deepClone(this.bbox), 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);
} }
} }

View File

@ -64,7 +64,7 @@ type Util = {
export const $canvasManager = atom<CanvasManager | null>(null); export const $canvasManager = atom<CanvasManager | null>(null);
export class CanvasManager { export class CanvasManager {
private static BBOX_PADDING_PX = 5; static BBOX_PADDING_PX = 5;
static BBOX_DEBOUNCE_MS = 300; static BBOX_DEBOUNCE_MS = 300;
stage: Konva.Stage; stage: Konva.Stage;
@ -82,13 +82,15 @@ export class CanvasManager {
log: Logger; log: Logger;
workerLog: Logger; workerLog: Logger;
_isDebugging: boolean;
onTransform: ((isTransforming: boolean) => void) | null; onTransform: ((isTransforming: boolean) => void) | null;
private store: Store<RootState>; _store: Store<RootState>;
private isFirstRender: boolean; _isFirstRender: boolean;
private prevState: CanvasV2State; _prevState: CanvasV2State;
private worker: Worker; _worker: Worker;
private tasks: Map<string, { task: GetBboxTask; onComplete: (extents: Extents | null) => void }>; _tasks: Map<string, { task: GetBboxTask; onComplete: (extents: Extents | null) => void }>;
constructor( constructor(
stage: Konva.Stage, stage: Konva.Stage,
@ -99,10 +101,10 @@ export class CanvasManager {
) { ) {
this.stage = stage; this.stage = stage;
this.container = container; this.container = container;
this.store = store; this._store = store;
this.stateApi = new CanvasStateApi(this.store); this.stateApi = new CanvasStateApi(this._store);
this.prevState = this.stateApi.getState(); this._prevState = this.stateApi.getState();
this.isFirstRender = true; this._isFirstRender = true;
this.log = logger('canvas'); this.log = logger('canvas');
this.workerLog = logger('worker'); this.workerLog = logger('worker');
@ -133,9 +135,9 @@ export class CanvasManager {
this.initialImage = new CanvasInitialImage(this.stateApi.getInitialImageState(), this); this.initialImage = new CanvasInitialImage(this.stateApi.getInitialImageState(), this);
this.stage.add(this.initialImage.konva.layer); this.stage.add(this.initialImage.konva.layer);
this.worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module', name: 'worker' }); this._worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module', name: 'worker' });
this.tasks = new Map(); this._tasks = new Map();
this.worker.onmessage = (event: MessageEvent<ExtentsResult | WorkerLogMessage>) => { this._worker.onmessage = (event: MessageEvent<ExtentsResult | WorkerLogMessage>) => {
const { type, data } = event.data; const { type, data } = event.data;
if (type === 'log') { if (type === 'log') {
if (data.ctx) { if (data.ctx) {
@ -144,20 +146,30 @@ export class CanvasManager {
this.workerLog[data.level](data.message); this.workerLog[data.level](data.message);
} }
} else if (type === 'extents') { } else if (type === 'extents') {
const task = this.tasks.get(data.id); const task = this._tasks.get(data.id);
if (!task) { if (!task) {
return; return;
} }
task.onComplete(data.extents); task.onComplete(data.extents);
} }
}; };
this.worker.onerror = (event) => { this._worker.onerror = (event) => {
this.log.error({ message: event.message }, 'Worker error'); this.log.error({ message: event.message }, 'Worker error');
}; };
this.worker.onmessageerror = () => { this._worker.onmessageerror = () => {
this.log.error('Worker message error'); this.log.error('Worker message error');
}; };
this.onTransform = null; this.onTransform = null;
this._isDebugging = false;
}
enableDebugging() {
this._isDebugging = true;
this.logDebugInfo();
}
disableDebugging() {
this._isDebugging = false;
} }
getLogger(namespace: string) { getLogger(namespace: string) {
@ -171,8 +183,8 @@ export class CanvasManager {
type: 'get_bbox', type: 'get_bbox',
data: { ...data, id }, data: { ...data, id },
}; };
this.tasks.set(id, { task, onComplete }); this._tasks.set(id, { task, onComplete });
this.worker.postMessage(task, [data.buffer]); this._worker.postMessage(task, [data.buffer]);
} }
async renderInitialImage() { async renderInitialImage() {
@ -306,12 +318,12 @@ export class CanvasManager {
render = async () => { render = async () => {
const state = this.stateApi.getState(); 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'); this.log.trace('No changes detected, skipping render');
return; 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'); this.log.debug('Rendering layers');
for (const canvasLayer of this.layers.values()) { for (const canvasLayer of this.layers.values()) {
@ -339,9 +351,9 @@ export class CanvasManager {
} }
if ( if (
this.isFirstRender || this._isFirstRender ||
state.tool.selected !== this.prevState.tool.selected || state.tool.selected !== this._prevState.tool.selected ||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
) { ) {
this.log.debug('Updating interaction'); this.log.debug('Updating interaction');
for (const layer of this.layers.values()) { for (const layer of this.layers.values()) {
@ -350,89 +362,89 @@ export class CanvasManager {
} }
if ( if (
this.isFirstRender || this._isFirstRender ||
state.initialImage !== this.prevState.initialImage || state.initialImage !== this._prevState.initialImage ||
state.bbox.rect !== this.prevState.bbox.rect || state.bbox.rect !== this._prevState.bbox.rect ||
state.tool.selected !== this.prevState.tool.selected || state.tool.selected !== this._prevState.tool.selected ||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
) { ) {
this.log.debug('Rendering initial image'); this.log.debug('Rendering initial image');
await this.renderInitialImage(); await this.renderInitialImage();
} }
if ( if (
this.isFirstRender || this._isFirstRender ||
state.regions.entities !== this.prevState.regions.entities || state.regions.entities !== this._prevState.regions.entities ||
state.settings.maskOpacity !== this.prevState.settings.maskOpacity || state.settings.maskOpacity !== this._prevState.settings.maskOpacity ||
state.tool.selected !== this.prevState.tool.selected || state.tool.selected !== this._prevState.tool.selected ||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
) { ) {
this.log.debug('Rendering regions'); this.log.debug('Rendering regions');
await this.renderRegions(); await this.renderRegions();
} }
if ( if (
this.isFirstRender || this._isFirstRender ||
state.inpaintMask !== this.prevState.inpaintMask || state.inpaintMask !== this._prevState.inpaintMask ||
state.settings.maskOpacity !== this.prevState.settings.maskOpacity || state.settings.maskOpacity !== this._prevState.settings.maskOpacity ||
state.tool.selected !== this.prevState.tool.selected || state.tool.selected !== this._prevState.tool.selected ||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
) { ) {
this.log.debug('Rendering inpaint mask'); this.log.debug('Rendering inpaint mask');
await this.renderInpaintMask(); await this.renderInpaintMask();
} }
if ( if (
this.isFirstRender || this._isFirstRender ||
state.controlAdapters.entities !== this.prevState.controlAdapters.entities || state.controlAdapters.entities !== this._prevState.controlAdapters.entities ||
state.tool.selected !== this.prevState.tool.selected || state.tool.selected !== this._prevState.tool.selected ||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
) { ) {
this.log.debug('Rendering control adapters'); this.log.debug('Rendering control adapters');
await this.renderControlAdapters(); await this.renderControlAdapters();
} }
if ( if (
this.isFirstRender || this._isFirstRender ||
state.bbox !== this.prevState.bbox || state.bbox !== this._prevState.bbox ||
state.tool.selected !== this.prevState.tool.selected || state.tool.selected !== this._prevState.tool.selected ||
state.session.isActive !== this.prevState.session.isActive state.session.isActive !== this._prevState.session.isActive
) { ) {
this.log.debug('Rendering generation bbox'); this.log.debug('Rendering generation bbox');
await this.preview.bbox.render(); await this.preview.bbox.render();
} }
if ( if (
this.isFirstRender || this._isFirstRender ||
state.layers !== this.prevState.layers || state.layers !== this._prevState.layers ||
state.controlAdapters !== this.prevState.controlAdapters || state.controlAdapters !== this._prevState.controlAdapters ||
state.regions !== this.prevState.regions state.regions !== this._prevState.regions
) { ) {
// this.log.debug('Updating entity bboxes'); // this.log.debug('Updating entity bboxes');
// debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged); // 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'); this.log.debug('Rendering staging area');
await this.preview.stagingArea.render(); await this.preview.stagingArea.render();
} }
if ( if (
this.isFirstRender || this._isFirstRender ||
state.layers.entities !== this.prevState.layers.entities || state.layers.entities !== this._prevState.layers.entities ||
state.controlAdapters.entities !== this.prevState.controlAdapters.entities || state.controlAdapters.entities !== this._prevState.controlAdapters.entities ||
state.regions.entities !== this.prevState.regions.entities || state.regions.entities !== this._prevState.regions.entities ||
state.inpaintMask !== this.prevState.inpaintMask || state.inpaintMask !== this._prevState.inpaintMask ||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
) { ) {
this.log.debug('Arranging entities'); this.log.debug('Arranging entities');
await this.arrangeEntities(); await this.arrangeEntities();
} }
this.prevState = state; this._prevState = state;
if (this.isFirstRender) { if (this._isFirstRender) {
this.isFirstRender = false; this._isFirstRender = false;
} }
}; };
@ -448,7 +460,7 @@ export class CanvasManager {
resizeObserver.observe(this.container); resizeObserver.observe(this.container);
this.fitStageToContainer(); 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 // When we this flag, we need to render the staging area
$shouldShowStagedImage.subscribe(async (shouldShowStagedImage, prevShouldShowStagedImage) => { $shouldShowStagedImage.subscribe(async (shouldShowStagedImage, prevShouldShowStagedImage) => {

View File

@ -26,7 +26,7 @@ export class CanvasRect {
this.id = id; this.id = id;
this.parent = parent; this.parent = parent;
this.parent.log.trace(`Creating rect ${this.id}`); this.parent._log.trace(`Creating rect ${this.id}`);
this.konva = { this.konva = {
group: new Konva.Group({ name: CanvasRect.GROUP_NAME, listening: false }), 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> { async update(state: RectShape, force?: boolean): Promise<boolean> {
if (this.state !== state || force) { 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; const { x, y, width, height, color } = state;
this.konva.rect.setAttrs({ this.konva.rect.setAttrs({
x, x,
@ -64,7 +64,7 @@ export class CanvasRect {
} }
destroy() { destroy() {
this.parent.log.trace(`Destroying rect ${this.id}`); this.parent._log.trace(`Destroying rect ${this.id}`);
this.konva.group.destroy(); this.konva.group.destroy();
} }
} }