fix(ui): staging area works

This commit is contained in:
psychedelicious 2024-08-08 17:24:16 +10:00
parent 30d318d021
commit 4668ea449b
13 changed files with 227 additions and 258 deletions

View File

@ -5,9 +5,10 @@ import { parseify } from 'common/util/serialize';
import { import {
caImageChanged, caImageChanged,
ipaImageChanged, ipaImageChanged,
layerAddedFromImage, layerAdded,
rgIPAdapterImageChanged, rgIPAdapterImageChanged,
} from 'features/controlLayers/store/canvasV2Slice'; } from 'features/controlLayers/store/canvasV2Slice';
import type { CanvasLayerState } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/types';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { isValidDrop } from 'features/dnd/util/isValidDrop'; import { isValidDrop } from 'features/dnd/util/isValidDrop';
@ -106,7 +107,13 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
activeData.payloadType === 'IMAGE_DTO' && activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO activeData.payload.imageDTO
) { ) {
dispatch(layerAddedFromImage({ imageObject: imageDTOToImageObject(activeData.payload.imageDTO) })); const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
const { x, y } = getState().canvasV2.bbox.rect;
const overrides: Partial<CanvasLayerState> = {
objects: [imageObject],
position: { x, y },
};
dispatch(layerAdded({ overrides, isSelected: true }));
return; return;
} }

View File

@ -15,7 +15,7 @@ export const AddLayerButton = memo(() => {
dispatch(rgAdded()); dispatch(rgAdded());
}, [dispatch]); }, [dispatch]);
const addRasterLayer = useCallback(() => { const addRasterLayer = useCallback(() => {
dispatch(layerAdded()); dispatch(layerAdded({ isSelected: true }));
}, [dispatch]); }, [dispatch]);
return ( return (

View File

@ -1,5 +1,6 @@
import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple'; import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import type { CanvasPreview } from 'features/controlLayers/konva/CanvasPreview';
import type { Rect } from 'features/controlLayers/store/types'; import type { Rect } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
import { atom } from 'nanostores'; import { atom } from 'nanostores';
@ -23,6 +24,7 @@ export class CanvasBbox {
static CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; static CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
static NO_ANCHORS: string[] = []; static NO_ANCHORS: string[] = [];
parent: CanvasPreview;
manager: CanvasManager; manager: CanvasManager;
konva: { konva: {
@ -31,8 +33,9 @@ export class CanvasBbox {
transformer: Konva.Transformer; transformer: Konva.Transformer;
}; };
constructor(manager: CanvasManager) { constructor(parent: CanvasPreview) {
this.manager = manager; this.parent = parent;
this.manager = this.parent.manager;
// Create a stash to hold onto the last aspect ratio of the bbox - this allows for locking the aspect ratio when // Create a stash to hold onto the last aspect ratio of the bbox - this allows for locking the aspect ratio when
// transforming the bbox. // transforming the bbox.
const bbox = this.manager.stateApi.getBbox(); const bbox = this.manager.stateApi.getBbox();

View File

@ -2,6 +2,7 @@ import { Mutex } from 'async-mutex';
import { deepClone } from 'common/util/deepClone'; import { deepClone } from 'common/util/deepClone';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
import type { CanvasStagingArea } from 'features/controlLayers/konva/CanvasStagingArea';
import { FILTER_MAP } from 'features/controlLayers/konva/filters'; import { FILTER_MAP } from 'features/controlLayers/konva/filters';
import { loadImage } from 'features/controlLayers/konva/util'; import { loadImage } from 'features/controlLayers/konva/util';
import type { CanvasImageState, GetLoggingContext } from 'features/controlLayers/store/types'; import type { CanvasImageState, GetLoggingContext } from 'features/controlLayers/store/types';
@ -19,7 +20,7 @@ export class CanvasImageRenderer {
static PLACEHOLDER_TEXT_NAME = `${CanvasImageRenderer.TYPE}_placeholder-text`; static PLACEHOLDER_TEXT_NAME = `${CanvasImageRenderer.TYPE}_placeholder-text`;
id: string; id: string;
parent: CanvasObjectRenderer; parent: CanvasObjectRenderer | CanvasStagingArea;
manager: CanvasManager; manager: CanvasManager;
log: Logger; log: Logger;
getLoggingContext: GetLoggingContext; getLoggingContext: GetLoggingContext;
@ -36,7 +37,7 @@ export class CanvasImageRenderer {
isError: boolean = false; isError: boolean = false;
mutex = new Mutex(); mutex = new Mutex();
constructor(state: CanvasImageState, parent: CanvasObjectRenderer) { constructor(state: CanvasImageState, parent: CanvasObjectRenderer | CanvasStagingArea) {
const { id, image } = state; const { id, image } = state;
const { width, height } = image; const { width, height } = image;
this.id = id; this.id = id;
@ -97,18 +98,16 @@ export class CanvasImageRenderer {
this.onFailedToLoadImage(); this.onFailedToLoadImage();
return; return;
} }
// Load the thumbnail first, but let the image load in parallel
loadImage(imageDTO.thumbnail_url) loadImage(imageDTO.thumbnail_url)
.then((thumbnailElement) => { .then((thumbnailElement) => {
this.thumbnailElement = thumbnailElement; this.thumbnailElement = thumbnailElement;
this.mutex.runExclusive(this.updateImageElement); this.updateImageElement();
})
.catch(this.onFailedToLoadImage);
loadImage(imageDTO.image_url)
.then((imageElement) => {
this.imageElement = imageElement;
this.mutex.runExclusive(this.updateImageElement);
}) })
.catch(this.onFailedToLoadImage); .catch(this.onFailedToLoadImage);
this.imageElement = await loadImage(imageDTO.image_url);
await this.updateImageElement();
} catch { } catch {
this.onFailedToLoadImage(); this.onFailedToLoadImage();
} }
@ -123,21 +122,29 @@ export class CanvasImageRenderer {
this.konva.placeholder.group.visible(true); this.konva.placeholder.group.visible(true);
}; };
updateImageElement = () => { updateImageElement = async () => {
const release = await this.mutex.acquire();
try {
const element = this.imageElement ?? this.thumbnailElement; const element = this.imageElement ?? this.thumbnailElement;
const { width, height } = this.state.image;
if (element) { if (element) {
if (this.konva.image && this.konva.image.image() !== element) { if (this.konva.image) {
this.log.trace('Updating Konva image attrs');
this.konva.image.setAttrs({ this.konva.image.setAttrs({
image: element, image: element,
width,
height,
}); });
} else { } else {
this.log.trace('Creating new Konva image');
this.konva.image = new Konva.Image({ this.konva.image = new Konva.Image({
name: CanvasImageRenderer.IMAGE_NAME, name: CanvasImageRenderer.IMAGE_NAME,
listening: false, listening: false,
image: element, image: element,
width: this.state.image.width, width,
height: this.state.image.height, height,
}); });
this.konva.group.add(this.konva.image); this.konva.group.add(this.konva.image);
} }
@ -150,10 +157,16 @@ export class CanvasImageRenderer {
this.konva.image.filters([]); this.konva.image.filters([]);
} }
this.konva.placeholder.rect.setAttrs({ width, height });
this.konva.placeholder.text.setAttrs({ width, height, fontSize: width / 16 });
this.isLoading = false; this.isLoading = false;
this.isError = false; this.isError = false;
this.konva.placeholder.group.visible(false); this.konva.placeholder.group.visible(false);
} }
} finally {
release();
}
}; };
update = async (state: CanvasImageState, force = false): Promise<boolean> => { update = async (state: CanvasImageState, force = false): Promise<boolean> => {
@ -173,8 +186,6 @@ export class CanvasImageRenderer {
this.konva.image?.clearCache(); this.konva.image?.clearCache();
this.konva.image?.filters([]); this.konva.image?.filters([]);
} }
this.konva.placeholder.rect.setAttrs({ width, height });
this.konva.placeholder.text.setAttrs({ width, height, fontSize: width / 16 });
this.state = state; this.state = state;
return true; return true;
} }

View File

@ -7,7 +7,6 @@ import type { CanvasBrushLineRenderer } from 'features/controlLayers/konva/Canva
import type { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; import type { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine';
import type { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage';
import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
import { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview';
import type { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; import type { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect';
import type { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import type { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer';
import { MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/controlLayers/konva/constants'; import { MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/controlLayers/konva/constants';
@ -19,7 +18,6 @@ import {
nanoid, nanoid,
} from 'features/controlLayers/konva/util'; } from 'features/controlLayers/konva/util';
import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker'; import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker';
import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice';
import type { import type {
CanvasControlAdapterState, CanvasControlAdapterState,
CanvasEntityIdentifier, CanvasEntityIdentifier,
@ -49,14 +47,12 @@ import type { ImageCategory, ImageDTO } from 'services/api/types';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
import { CanvasBackground } from './CanvasBackground'; import { CanvasBackground } from './CanvasBackground';
import { CanvasBbox } from './CanvasBbox';
import { CanvasControlAdapter } from './CanvasControlAdapter'; import { CanvasControlAdapter } from './CanvasControlAdapter';
import { CanvasLayerAdapter } from './CanvasLayerAdapter'; import { CanvasLayerAdapter } from './CanvasLayerAdapter';
import { CanvasMaskAdapter } from './CanvasMaskAdapter'; import { CanvasMaskAdapter } from './CanvasMaskAdapter';
import { CanvasPreview } from './CanvasPreview'; import { CanvasPreview } from './CanvasPreview';
import { CanvasStagingArea } from './CanvasStagingArea'; import { CanvasStagingArea } from './CanvasStagingArea';
import { CanvasStateApi } from './CanvasStateApi'; import { CanvasStateApi } from './CanvasStateApi';
import { CanvasTool } from './CanvasTool';
import { setStageEventHandlers } from './events'; import { setStageEventHandlers } from './events';
// type Extents = { // type Extents = {
@ -159,15 +155,6 @@ export class CanvasManager {
this.transformingEntity = new PubSub<CanvasEntityIdentifier | null>(null); this.transformingEntity = new PubSub<CanvasEntityIdentifier | null>(null);
this.toolState = new PubSub(this.stateApi.getToolState()); this.toolState = new PubSub(this.stateApi.getToolState());
this.currentFill = new PubSub(this.getCurrentFill());
this.selectedEntityIdentifier = new PubSub(
this.stateApi.getState().selectedEntityIdentifier,
(a, b) => a?.id === b?.id
);
this.selectedEntity = new PubSub(
this.getSelectedEntity(),
(a, b) => a?.state === b?.state && a?.adapter === b?.adapter
);
this._prevState = this.stateApi.getState(); this._prevState = this.stateApi.getState();
@ -187,13 +174,8 @@ export class CanvasManager {
uploadImage, uploadImage,
}; };
this.preview = new CanvasPreview( this.preview = new CanvasPreview(this);
new CanvasBbox(this), this.stage.add(this.preview.getLayer());
new CanvasTool(this),
new CanvasStagingArea(this),
new CanvasProgressPreview(this)
);
this.stage.add(this.preview.layer);
this.background = new CanvasBackground(this); this.background = new CanvasBackground(this);
this.stage.add(this.background.konva.layer); this.stage.add(this.background.konva.layer);
@ -226,6 +208,16 @@ export class CanvasManager {
this.log.error('Worker message error'); this.log.error('Worker message error');
}; };
this.currentFill = new PubSub(this.getCurrentFill());
this.selectedEntityIdentifier = new PubSub(
this.stateApi.getState().selectedEntityIdentifier,
(a, b) => a?.id === b?.id
);
this.selectedEntity = new PubSub(
this.getSelectedEntity(),
(a, b) => a?.state === b?.state && a?.adapter === b?.adapter
);
this.inpaintMask = new CanvasMaskAdapter(this.stateApi.getInpaintMaskState(), this); this.inpaintMask = new CanvasMaskAdapter(this.stateApi.getInpaintMaskState(), this);
this.stage.add(this.inpaintMask.konva.layer); this.stage.add(this.inpaintMask.konva.layer);
} }
@ -249,10 +241,6 @@ export class CanvasManager {
this._worker.postMessage(task, [data.buffer]); this._worker.postMessage(task, [data.buffer]);
} }
async renderProgressPreview() {
await this.preview.progressPreview.render(this.stateApi.$lastProgressEvent.get());
}
async renderControlAdapters() { async renderControlAdapters() {
const { entities } = this.stateApi.getControlAdaptersState(); const { entities } = this.stateApi.getControlAdaptersState();
@ -291,7 +279,7 @@ export class CanvasManager {
this.regions.get(rg.id)?.konva.layer.zIndex(++zIndex); this.regions.get(rg.id)?.konva.layer.zIndex(++zIndex);
} }
this.inpaintMask.konva.layer.zIndex(++zIndex); this.inpaintMask.konva.layer.zIndex(++zIndex);
this.preview.layer.zIndex(++zIndex); this.preview.getLayer().zIndex(++zIndex);
} }
fitStageToContainer() { fitStageToContainer() {
@ -611,25 +599,6 @@ export class CanvasManager {
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
const unsubscribeShouldShowStagedImage = $shouldShowStagedImage.subscribe(
async (shouldShowStagedImage, prevShouldShowStagedImage) => {
if (shouldShowStagedImage !== prevShouldShowStagedImage) {
this.log.debug('Rendering staging area');
await this.preview.stagingArea.render();
}
}
);
const unsubscribeLastProgressEvent = $lastProgressEvent.subscribe(
async (lastProgressEvent, prevLastProgressEvent) => {
if (lastProgressEvent !== prevLastProgressEvent) {
this.log.debug('Rendering progress image');
await this.preview.progressPreview.render(lastProgressEvent);
}
}
);
this.log.debug('First render of konva stage'); this.log.debug('First render of konva stage');
this.preview.tool.render(); this.preview.tool.render();
this.render(); this.render();
@ -650,8 +619,6 @@ export class CanvasManager {
this.preview.destroy(); this.preview.destroy();
unsubscribeRenderer(); unsubscribeRenderer();
unsubscribeListeners(); unsubscribeListeners();
unsubscribeShouldShowStagedImage();
unsubscribeLastProgressEvent();
resizeObserver.disconnect(); resizeObserver.disconnect();
}; };
}; };

View File

@ -1,40 +1,51 @@
import type { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasProgressImage } from 'features/controlLayers/konva/CanvasProgressImage';
import Konva from 'konva'; import Konva from 'konva';
import type { CanvasBbox } from './CanvasBbox'; import { CanvasBbox } from './CanvasBbox';
import type { CanvasStagingArea } from './CanvasStagingArea'; import { CanvasStagingArea } from './CanvasStagingArea';
import type { CanvasTool } from './CanvasTool'; import { CanvasTool } from './CanvasTool';
export class CanvasPreview { export class CanvasPreview {
manager: CanvasManager;
konva: {
layer: Konva.Layer; layer: Konva.Layer;
};
tool: CanvasTool; tool: CanvasTool;
bbox: CanvasBbox; bbox: CanvasBbox;
stagingArea: CanvasStagingArea; stagingArea: CanvasStagingArea;
progressPreview: CanvasProgressPreview; progressImage: CanvasProgressImage;
constructor( constructor(manager: CanvasManager) {
bbox: CanvasBbox, this.manager = manager;
tool: CanvasTool, this.konva = {
stagingArea: CanvasStagingArea, layer: new Konva.Layer({ listening: true, imageSmoothingEnabled: false }),
progressPreview: CanvasProgressPreview };
) {
this.layer = new Konva.Layer({ listening: true, imageSmoothingEnabled: false });
this.stagingArea = stagingArea; this.stagingArea = new CanvasStagingArea(this);
this.layer.add(this.stagingArea.konva.group); this.konva.layer.add(...this.stagingArea.getNodes());
this.bbox = bbox; this.progressImage = new CanvasProgressImage(this);
this.layer.add(this.bbox.konva.group); this.konva.layer.add(...this.progressImage.getNodes());
this.tool = tool; this.bbox = new CanvasBbox(this);
this.layer.add(this.tool.konva.group); this.konva.layer.add(this.bbox.konva.group);
this.progressPreview = progressPreview; this.tool = new CanvasTool(this);
this.layer.add(this.progressPreview.konva.group); this.konva.layer.add(this.tool.konva.group);
} }
getLayer = () => {
return this.konva.layer;
};
destroy() { destroy() {
// this.stagingArea.destroy(); // TODO(psyche): implement destroy
this.progressImage.destroy();
// this.bbox.destroy(); // TODO(psyche): implement destroy
this.tool.destroy(); this.tool.destroy();
this.layer.destroy(); this.konva.layer.destroy();
} }
} }

View File

@ -1,5 +1,9 @@
import { loadImage } from 'features/controlLayers/konva/util'; import { Mutex } from 'async-mutex';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import type { CanvasPreview } from 'features/controlLayers/konva/CanvasPreview';
import { getPrefixedId, loadImage } from 'features/controlLayers/konva/util';
import Konva from 'konva'; import Konva from 'konva';
import type { InvocationDenoiseProgressEvent } from 'services/events/types';
export class CanvasProgressImage { export class CanvasProgressImage {
static NAME_PREFIX = 'progress-image'; static NAME_PREFIX = 'progress-image';
@ -7,53 +11,86 @@ export class CanvasProgressImage {
static IMAGE_NAME = `${CanvasProgressImage.NAME_PREFIX}_image`; static IMAGE_NAME = `${CanvasProgressImage.NAME_PREFIX}_image`;
id: string; id: string;
progressImageId: string | null; parent: CanvasPreview;
manager: CanvasManager;
/**
* A set of subscriptions that should be cleaned up when the transformer is destroyed.
*/
subscriptions: Set<() => void> = new Set();
progressImageId: string | null = null;
konva: { konva: {
group: Konva.Group; group: Konva.Group;
image: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately image: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately
}; };
isLoading: boolean; isLoading: boolean = false;
isError: boolean; isError: boolean = false;
imageElement: HTMLImageElement | null = null;
constructor(arg: { id: string }) { lastProgressEvent: InvocationDenoiseProgressEvent | null = null;
const { id } = arg;
mutex: Mutex = new Mutex();
constructor(parent: CanvasPreview) {
this.id = getPrefixedId(CanvasProgressImage.NAME_PREFIX);
this.parent = parent;
this.manager = parent.manager;
this.konva = { this.konva = {
group: new Konva.Group({ name: CanvasProgressImage.GROUP_NAME, listening: false }), group: new Konva.Group({ name: CanvasProgressImage.GROUP_NAME, listening: false }),
image: null, image: null,
}; };
this.id = id;
this.progressImageId = null; this.manager.stateApi.$lastProgressEvent.listen((event) => {
this.isLoading = false; this.lastProgressEvent = event;
this.isError = false; this.render();
});
} }
async updateImageSource( getNodes = () => {
progressImageId: string, return [this.konva.group];
dataURL: string, };
x: number,
y: number, render = async () => {
width: number, const release = await this.mutex.acquire();
height: number
) { if (!this.lastProgressEvent) {
if (this.isLoading) { this.konva.group.visible(false);
this.imageElement = null;
this.isLoading = false;
this.isError = false;
release();
return; return;
} }
const { isStaging } = this.manager.stateApi.getSession();
if (!isStaging) {
release();
return;
}
this.isLoading = true; this.isLoading = true;
const { x, y } = this.manager.stateApi.getBbox().rect;
const { dataURL, width, height } = this.lastProgressEvent.progress_image;
try { try {
const imageEl = await loadImage(dataURL); this.imageElement = await loadImage(dataURL);
if (this.konva.image) { if (this.konva.image) {
console.log('UPDATING PROGRESS IMAGE')
this.konva.image.setAttrs({ this.konva.image.setAttrs({
image: imageEl, image: this.imageElement,
x, x,
y, y,
width, width,
height, height,
}); });
} else { } else {
console.log('CREATING NEW PROGRESS IMAGE')
this.konva.image = new Konva.Image({ this.konva.image = new Konva.Image({
name: CanvasProgressImage.IMAGE_NAME, name: CanvasProgressImage.IMAGE_NAME,
listening: false, listening: false,
image: imageEl, image: this.imageElement,
x, x,
y, y,
width, width,
@ -61,14 +98,19 @@ export class CanvasProgressImage {
}); });
this.konva.group.add(this.konva.image); this.konva.group.add(this.konva.image);
} }
this.isLoading = false; this.konva.group.visible(true);
this.id = progressImageId;
} catch { } catch {
this.isError = true; this.isError = true;
} finally {
this.isLoading = false;
release();
} }
} };
destroy() { destroy = () => {
this.konva.group.destroy(); for (const unsubscribe of this.subscriptions) {
unsubscribe();
} }
this.konva.group.destroy();
};
} }

View File

@ -1,46 +0,0 @@
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasProgressImage } from 'features/controlLayers/konva/CanvasProgressImage';
import Konva from 'konva';
import type { InvocationDenoiseProgressEvent } from 'services/events/types';
export class CanvasProgressPreview {
static NAME_PREFIX = 'progress-preview';
static GROUP_NAME = `${CanvasProgressPreview.NAME_PREFIX}_group`;
konva: {
group: Konva.Group;
progressImage: CanvasProgressImage;
};
manager: CanvasManager;
constructor(manager: CanvasManager) {
this.manager = manager;
this.konva = {
group: new Konva.Group({ name: CanvasProgressPreview.GROUP_NAME, listening: false }),
progressImage: new CanvasProgressImage({ id: 'progress-image' }),
};
this.konva.group.add(this.konva.progressImage.konva.group);
}
async render(lastProgressEvent: InvocationDenoiseProgressEvent | null) {
const bboxRect = this.manager.stateApi.getBbox().rect;
const session = this.manager.stateApi.getSession();
if (lastProgressEvent && session.isStaging) {
const { invocation, step, progress_image } = lastProgressEvent;
const { dataURL } = progress_image;
const { x, y, width, height } = bboxRect;
const progressImageId = `${invocation.id}_${step}`;
if (
!this.konva.progressImage.isLoading &&
!this.konva.progressImage.isError &&
this.konva.progressImage.progressImageId !== progressImageId
) {
await this.konva.progressImage.updateImageSource(progressImageId, dataURL, x, y, width, height);
this.konva.progressImage.konva.group.visible(true);
}
} else {
this.konva.progressImage.konva.group.visible(false);
}
}
}

View File

@ -1,5 +1,6 @@
import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import type { CanvasPreview } from 'features/controlLayers/konva/CanvasPreview';
import { getPrefixedId } from 'features/controlLayers/konva/util'; import { getPrefixedId } from 'features/controlLayers/konva/util';
import type { GetLoggingContext, StagingAreaImage } from 'features/controlLayers/store/types'; import type { GetLoggingContext, StagingAreaImage } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
@ -10,6 +11,7 @@ export class CanvasStagingArea {
static GROUP_NAME = `${CanvasStagingArea.TYPE}_group`; static GROUP_NAME = `${CanvasStagingArea.TYPE}_group`;
id: string; id: string;
parent: CanvasPreview;
manager: CanvasManager; manager: CanvasManager;
log: Logger; log: Logger;
getLoggingContext: GetLoggingContext; getLoggingContext: GetLoggingContext;
@ -19,9 +21,15 @@ export class CanvasStagingArea {
image: CanvasImageRenderer | null; image: CanvasImageRenderer | null;
selectedImage: StagingAreaImage | null; selectedImage: StagingAreaImage | null;
constructor(manager: CanvasManager) { /**
* A set of subscriptions that should be cleaned up when the transformer is destroyed.
*/
subscriptions: Set<() => void> = new Set();
constructor(parent: CanvasPreview) {
this.id = getPrefixedId(CanvasStagingArea.TYPE); this.id = getPrefixedId(CanvasStagingArea.TYPE);
this.manager = manager; this.parent = parent;
this.manager = this.parent.manager;
this.getLoggingContext = this.manager.buildGetLoggingContext(this); this.getLoggingContext = this.manager.buildGetLoggingContext(this);
this.log = this.manager.buildLogger(this.getLoggingContext); this.log = this.manager.buildLogger(this.getLoggingContext);
this.log.debug('Creating staging area'); this.log.debug('Creating staging area');
@ -29,14 +37,17 @@ export class CanvasStagingArea {
this.konva = { group: new Konva.Group({ name: CanvasStagingArea.GROUP_NAME, listening: false }) }; this.konva = { group: new Konva.Group({ name: CanvasStagingArea.GROUP_NAME, listening: false }) };
this.image = null; this.image = null;
this.selectedImage = null; this.selectedImage = null;
this.subscriptions.add(this.manager.stateApi.$shouldShowStagedImage.listen(this.render));
} }
render = async () => { render = async () => {
const session = this.manager.stateApi.getSession(); const session = this.manager.stateApi.getSession();
const bboxRect = this.manager.stateApi.getBbox().rect; const { rect } = this.manager.stateApi.getBbox();
const shouldShowStagedImage = this.manager.stateApi.$shouldShowStagedImage.get(); const shouldShowStagedImage = this.manager.stateApi.$shouldShowStagedImage.get();
this.selectedImage = session.stagedImages[session.selectedStagedImageIndex] ?? null; this.selectedImage = session.stagedImages[session.selectedStagedImageIndex] ?? null;
this.konva.group.position({ x: rect.x, y: rect.y });
if (this.selectedImage) { if (this.selectedImage) {
const { imageDTO, offsetX, offsetY } = this.selectedImage; const { imageDTO, offsetX, offsetY } = this.selectedImage;
@ -47,10 +58,6 @@ export class CanvasStagingArea {
{ {
id: 'staging-area-image', id: 'staging-area-image',
type: 'image', type: 'image',
x: 0,
y: 0,
width,
height,
filters: [], filters: [],
image: { image: {
image_name: image_name, image_name: image_name,
@ -63,11 +70,7 @@ export class CanvasStagingArea {
this.konva.group.add(this.image.konva.group); this.konva.group.add(this.image.konva.group);
} }
if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) { if (!this.image.isLoading && !this.image.isError) {
this.image.konva.image?.width(imageDTO.width);
this.image.konva.image?.height(imageDTO.height);
this.image.konva.group.x(bboxRect.x + offsetX);
this.image.konva.group.y(bboxRect.y + offsetY);
await this.image.updateImageSource(imageDTO.image_name); await this.image.updateImageSource(imageDTO.image_name);
this.manager.stateApi.$lastProgressEvent.set(null); this.manager.stateApi.$lastProgressEvent.set(null);
} }
@ -77,6 +80,22 @@ export class CanvasStagingArea {
} }
}; };
getNodes = () => {
return [this.konva.group];
};
destroy = () => {
if (this.image) {
this.image.destroy();
}
for (const unsubscribe of this.subscriptions) {
unsubscribe();
}
for (const node of this.getNodes()) {
node.destroy();
}
};
repr = () => { repr = () => {
return { return {
id: this.id, id: this.id,

View File

@ -1,5 +1,6 @@
import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import type { CanvasPreview } from 'features/controlLayers/konva/CanvasPreview';
import { import {
BRUSH_BORDER_INNER_COLOR, BRUSH_BORDER_INNER_COLOR,
BRUSH_BORDER_OUTER_COLOR, BRUSH_BORDER_OUTER_COLOR,
@ -25,6 +26,7 @@ export class CanvasTool {
static ERASER_INNER_BORDER_CIRCLE_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_inner-border-circle`; static ERASER_INNER_BORDER_CIRCLE_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_inner-border-circle`;
static ERASER_OUTER_BORDER_CIRCLE_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_outer-border-circle`; static ERASER_OUTER_BORDER_CIRCLE_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_outer-border-circle`;
parent: CanvasPreview;
manager: CanvasManager; manager: CanvasManager;
konva: { konva: {
group: Konva.Group; group: Konva.Group;
@ -47,8 +49,9 @@ export class CanvasTool {
*/ */
subscriptions: Set<() => void> = new Set(); subscriptions: Set<() => void> = new Set();
constructor(manager: CanvasManager) { constructor(parent: CanvasPreview) {
this.manager = manager; this.parent = parent;
this.manager = this.parent.manager;
this.konva = { this.konva = {
group: new Konva.Group({ name: CanvasTool.GROUP_NAME }), group: new Konva.Group({ name: CanvasTool.GROUP_NAME }),
brush: { brush: {

View File

@ -48,13 +48,6 @@ export const bboxReducers = {
state.bbox.aspectRatio.id = 'Free'; state.bbox.aspectRatio.id = 'Free';
state.bbox.aspectRatio.isLocked = false; state.bbox.aspectRatio.isLocked = false;
} }
if (!state.session.isActive) {
if (state.initialImage.imageObject) {
state.initialImage.imageObject.width = state.bbox.rect.width;
state.initialImage.imageObject.height = state.bbox.rect.height;
}
}
}, },
bboxHeightChanged: ( bboxHeightChanged: (
state, state,
@ -73,13 +66,6 @@ export const bboxReducers = {
state.bbox.aspectRatio.id = 'Free'; state.bbox.aspectRatio.id = 'Free';
state.bbox.aspectRatio.isLocked = false; state.bbox.aspectRatio.isLocked = false;
} }
if (!state.session.isActive) {
if (state.initialImage.imageObject) {
state.initialImage.imageObject.width = state.bbox.rect.width;
state.initialImage.imageObject.height = state.bbox.rect.height;
}
}
}, },
bboxAspectRatioLockToggled: (state) => { bboxAspectRatioLockToggled: (state) => {
state.bbox.aspectRatio.isLocked = !state.bbox.aspectRatio.isLocked; state.bbox.aspectRatio.isLocked = !state.bbox.aspectRatio.isLocked;
@ -99,12 +85,6 @@ export const bboxReducers = {
state.bbox.rect.width = width; state.bbox.rect.width = width;
state.bbox.rect.height = height; state.bbox.rect.height = height;
} }
if (!state.session.isActive) {
if (state.initialImage.imageObject) {
state.initialImage.imageObject.width = state.bbox.rect.width;
state.initialImage.imageObject.height = state.bbox.rect.height;
}
}
}, },
bboxDimensionsSwapped: (state) => { bboxDimensionsSwapped: (state) => {
state.bbox.aspectRatio.value = 1 / state.bbox.aspectRatio.value; state.bbox.aspectRatio.value = 1 / state.bbox.aspectRatio.value;
@ -122,12 +102,6 @@ export const bboxReducers = {
state.bbox.rect.height = height; state.bbox.rect.height = height;
state.bbox.aspectRatio.id = ASPECT_RATIO_MAP[state.bbox.aspectRatio.id].inverseID; state.bbox.aspectRatio.id = ASPECT_RATIO_MAP[state.bbox.aspectRatio.id].inverseID;
} }
if (!state.session.isActive) {
if (state.initialImage.imageObject) {
state.initialImage.imageObject.width = state.bbox.rect.width;
state.initialImage.imageObject.height = state.bbox.rect.height;
}
}
}, },
bboxSizeOptimized: (state) => { bboxSizeOptimized: (state) => {
const optimalDimension = getOptimalDimension(state.params.model); const optimalDimension = getOptimalDimension(state.params.model);
@ -140,11 +114,5 @@ export const bboxReducers = {
state.bbox.rect.width = optimalDimension; state.bbox.rect.width = optimalDimension;
state.bbox.rect.height = optimalDimension; state.bbox.rect.height = optimalDimension;
} }
if (!state.session.isActive) {
if (state.initialImage.imageObject) {
state.initialImage.imageObject.width = state.bbox.rect.width;
state.initialImage.imageObject.height = state.bbox.rect.height;
}
}
}, },
} satisfies SliceCaseReducers<CanvasV2State>; } satisfies SliceCaseReducers<CanvasV2State>;

View File

@ -411,7 +411,6 @@ export const {
bboxSizeOptimized, bboxSizeOptimized,
// layers // layers
layerAdded, layerAdded,
layerAddedFromImage,
layerRecalled, layerRecalled,
layerOpacityChanged, layerOpacityChanged,
layerAllDeleted, layerAllDeleted,

View File

@ -4,7 +4,7 @@ import { merge } from 'lodash-es';
import type { ImageDTO } from 'services/api/types'; import type { ImageDTO } from 'services/api/types';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
import type { CanvasImageState, CanvasLayerState, CanvasV2State } from './types'; import type { CanvasLayerState, CanvasV2State } from './types';
import { imageDTOToImageWithDims } from './types'; import { imageDTOToImageWithDims } from './types';
export const selectLayer = (state: CanvasV2State, id: string) => state.layers.entities.find((layer) => layer.id === id); export const selectLayer = (state: CanvasV2State, id: string) => state.layers.entities.find((layer) => layer.id === id);
@ -16,8 +16,11 @@ export const selectLayerOrThrow = (state: CanvasV2State, id: string) => {
export const layersReducers = { export const layersReducers = {
layerAdded: { layerAdded: {
reducer: (state, action: PayloadAction<{ id: string; overrides?: Partial<CanvasLayerState> }>) => { reducer: (
const { id } = action.payload; state,
action: PayloadAction<{ id: string; overrides?: Partial<CanvasLayerState>; isSelected?: boolean }>
) => {
const { id, overrides, isSelected } = action.payload;
const layer: CanvasLayerState = { const layer: CanvasLayerState = {
id, id,
type: 'layer', type: 'layer',
@ -27,12 +30,14 @@ export const layersReducers = {
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
imageCache: null, imageCache: null,
}; };
merge(layer, action.payload.overrides); merge(layer, overrides);
state.layers.entities.push(layer); state.layers.entities.push(layer);
if (isSelected) {
state.selectedEntityIdentifier = { type: 'layer', id }; state.selectedEntityIdentifier = { type: 'layer', id };
}
state.layers.imageCache = null; state.layers.imageCache = null;
}, },
prepare: (payload: { overrides?: Partial<CanvasLayerState> }) => ({ prepare: (payload: { overrides?: Partial<CanvasLayerState>; isSelected?: boolean }) => ({
payload: { ...payload, id: getPrefixedId('layer') }, payload: { ...payload, id: getPrefixedId('layer') },
}), }),
}, },
@ -42,26 +47,6 @@ export const layersReducers = {
state.selectedEntityIdentifier = { type: 'layer', id: data.id }; state.selectedEntityIdentifier = { type: 'layer', id: data.id };
state.layers.imageCache = null; state.layers.imageCache = null;
}, },
layerAddedFromImage: {
reducer: (state, action: PayloadAction<{ id: string; imageObject: CanvasImageState }>) => {
const { id, imageObject } = action.payload;
const layer: CanvasLayerState = {
id,
type: 'layer',
isEnabled: true,
objects: [imageObject],
opacity: 1,
position: { x: 0, y: 0 },
imageCache: null,
};
state.layers.entities.push(layer);
state.selectedEntityIdentifier = { type: 'layer', id };
state.layers.imageCache = null;
},
prepare: (payload: { imageObject: CanvasImageState }) => ({
payload: { ...payload, id: getPrefixedId('layer') },
}),
},
layerAllDeleted: (state) => { layerAllDeleted: (state) => {
state.layers.entities = []; state.layers.entities = [];
state.layers.imageCache = null; state.layers.imageCache = null;