feat(ui): split buffer renderer from object renderer

This commit is contained in:
psychedelicious
2024-09-08 17:24:21 +10:00
parent d7cde0fc23
commit d5cd50c3ea
14 changed files with 346 additions and 219 deletions

View File

@@ -1,6 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import type { SerializableObject } from 'common/types';
import { deepClone } from 'common/util/deepClone';
import type { CanvasEntityBufferObjectRenderer } from 'features/controlLayers/konva/CanvasEntityBufferObjectRenderer';
import type { CanvasEntityFilterer } from 'features/controlLayers/konva/CanvasEntityFilterer';
import type { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntityObjectRenderer';
import type { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer';
@@ -35,6 +36,11 @@ export abstract class CanvasEntityAdapterBase<
*/
abstract renderer: CanvasEntityObjectRenderer;
/**
* The buffer renderer for this entity adapter. All entities must have a buffer renderer.
*/
abstract bufferRenderer: CanvasEntityBufferObjectRenderer;
/**
* The filterer for this entity adapter. Entities that support filtering should implement this property.
*/

View File

@@ -1,5 +1,6 @@
import type { SerializableObject } from 'common/types';
import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapter/CanvasEntityAdapterBase';
import { CanvasEntityBufferObjectRenderer } from 'features/controlLayers/konva/CanvasEntityBufferObjectRenderer';
import { CanvasEntityFilterer } from 'features/controlLayers/konva/CanvasEntityFilterer';
import { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntityObjectRenderer';
import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer';
@@ -11,14 +12,16 @@ import { omit } from 'lodash-es';
export class CanvasEntityAdapterControlLayer extends CanvasEntityAdapterBase<CanvasControlLayerState> {
static TYPE = 'control_layer_adapter';
transformer: CanvasEntityTransformer;
renderer: CanvasEntityObjectRenderer;
bufferRenderer: CanvasEntityBufferObjectRenderer;
transformer: CanvasEntityTransformer;
filterer: CanvasEntityFilterer;
constructor(entityIdentifier: CanvasEntityIdentifier<'control_layer'>, manager: CanvasManager) {
super(entityIdentifier, manager, CanvasEntityAdapterControlLayer.TYPE);
this.renderer = new CanvasEntityObjectRenderer(this);
this.bufferRenderer = new CanvasEntityBufferObjectRenderer(this);
this.transformer = new CanvasEntityTransformer(this);
this.filterer = new CanvasEntityFilterer(this);

View File

@@ -1,5 +1,6 @@
import type { SerializableObject } from 'common/types';
import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapter/CanvasEntityAdapterBase';
import { CanvasEntityBufferObjectRenderer } from 'features/controlLayers/konva/CanvasEntityBufferObjectRenderer';
import { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntityObjectRenderer';
import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
@@ -10,14 +11,16 @@ import { omit } from 'lodash-es';
export class CanvasEntityAdapterInpaintMask extends CanvasEntityAdapterBase<CanvasInpaintMaskState> {
static TYPE = 'inpaint_mask_adapter';
transformer: CanvasEntityTransformer;
renderer: CanvasEntityObjectRenderer;
bufferRenderer: CanvasEntityBufferObjectRenderer;
transformer: CanvasEntityTransformer;
filterer = undefined;
constructor(entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>, manager: CanvasManager) {
super(entityIdentifier, manager, CanvasEntityAdapterInpaintMask.TYPE);
this.renderer = new CanvasEntityObjectRenderer(this);
this.bufferRenderer = new CanvasEntityBufferObjectRenderer(this);
this.transformer = new CanvasEntityTransformer(this);
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(this.selectState, this.sync));

View File

@@ -1,5 +1,6 @@
import type { SerializableObject } from 'common/types';
import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapter/CanvasEntityAdapterBase';
import { CanvasEntityBufferObjectRenderer } from 'features/controlLayers/konva/CanvasEntityBufferObjectRenderer';
import { CanvasEntityFilterer } from 'features/controlLayers/konva/CanvasEntityFilterer';
import { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntityObjectRenderer';
import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer';
@@ -11,14 +12,16 @@ import { omit } from 'lodash-es';
export class CanvasEntityAdapterRasterLayer extends CanvasEntityAdapterBase<CanvasRasterLayerState> {
static TYPE = 'raster_layer_adapter';
transformer: CanvasEntityTransformer;
renderer: CanvasEntityObjectRenderer;
bufferRenderer: CanvasEntityBufferObjectRenderer;
transformer: CanvasEntityTransformer;
filterer: CanvasEntityFilterer;
constructor(entityIdentifier: CanvasEntityIdentifier<'raster_layer'>, manager: CanvasManager) {
super(entityIdentifier, manager, CanvasEntityAdapterRasterLayer.TYPE);
this.renderer = new CanvasEntityObjectRenderer(this);
this.bufferRenderer = new CanvasEntityBufferObjectRenderer(this);
this.transformer = new CanvasEntityTransformer(this);
this.filterer = new CanvasEntityFilterer(this);

View File

@@ -1,5 +1,6 @@
import type { SerializableObject } from 'common/types';
import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapter/CanvasEntityAdapterBase';
import { CanvasEntityBufferObjectRenderer } from 'features/controlLayers/konva/CanvasEntityBufferObjectRenderer';
import { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntityObjectRenderer';
import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
@@ -10,14 +11,16 @@ import { omit } from 'lodash-es';
export class CanvasEntityAdapterRegionalGuidance extends CanvasEntityAdapterBase<CanvasRegionalGuidanceState> {
static TYPE = 'regional_guidance_adapter';
transformer: CanvasEntityTransformer;
renderer: CanvasEntityObjectRenderer;
bufferRenderer: CanvasEntityBufferObjectRenderer;
transformer: CanvasEntityTransformer;
filterer = undefined;
constructor(entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>, manager: CanvasManager) {
super(entityIdentifier, manager, CanvasEntityAdapterRegionalGuidance.TYPE);
this.renderer = new CanvasEntityObjectRenderer(this);
this.bufferRenderer = new CanvasEntityBufferObjectRenderer(this);
this.transformer = new CanvasEntityTransformer(this);
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(this.selectState, this.sync));

View File

@@ -0,0 +1,238 @@
import { deepClone } from 'common/util/deepClone';
import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntityAdapter/types';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import { CanvasObjectBrushLine } from 'features/controlLayers/konva/CanvasObjectBrushLine';
import { CanvasObjectEraserLine } from 'features/controlLayers/konva/CanvasObjectEraserLine';
import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObjectImage';
import { CanvasObjectRect } from 'features/controlLayers/konva/CanvasObjectRect';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import type {
CanvasBrushLineState,
CanvasEraserLineState,
CanvasImageState,
CanvasRectState,
} from 'features/controlLayers/store/types';
import Konva from 'konva';
import type { Logger } from 'roarr';
import { assert } from 'tsafe';
/**
* Union of all object renderers.
*/
type AnyObjectRenderer = CanvasObjectBrushLine | CanvasObjectEraserLine | CanvasObjectRect | CanvasObjectImage;
/**
* Union of all object states.
*/
type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImageState | CanvasRectState;
/**
* Handles rendering of objects for a canvas entity.
*/
export class CanvasEntityBufferObjectRenderer extends CanvasModuleBase {
readonly type = 'buffer_renderer';
readonly id: string;
readonly path: string[];
readonly parent: CanvasEntityAdapter;
readonly manager: CanvasManager;
readonly log: Logger;
/**
* A set of subscriptions that should be cleaned up when the transformer is destroyed.
*/
subscriptions: Set<() => void> = new Set();
/**
* A buffer object state that is rendered separately from the other objects. This is used for objects that are being
* drawn in real-time, such as brush lines. The buffer object state only exists in this renderer and is not part of
* the application state until it is committed.
*/
state: AnyObjectState | null = null;
/**
* The object renderer for the buffer object state. It is created when the buffer object state is set and destroyed
* when the buffer object state is cleared. This is separate from the other object renderers to allow the buffer to
* be rendered separately.
*/
renderer: AnyObjectRenderer | null = null;
/**
* A object containing singleton Konva nodes.
*/
konva: {
/**
* A Konva Group that holds the buffer object renderer.
*/
group: Konva.Group;
};
constructor(parent: CanvasEntityAdapter) {
super();
this.id = getPrefixedId(this.type);
this.parent = parent;
this.manager = parent.manager;
this.path = this.manager.buildPath(this);
this.log = this.manager.buildLogger(this);
this.log.debug('Creating module');
this.konva = {
group: new Konva.Group({ name: `${this.type}:buffer_group`, listening: false }),
};
this.parent.konva.layer.add(this.konva.group);
// When switching tool, commit the buffer. This is necessary to prevent the buffer from being lost when the
// user switches tool mid-drawing, for example by pressing space to pan the stage. It's easy to press space
// to pan _before_ releasing the mouse button, which would cause the buffer to be lost if we didn't commit it.
this.subscriptions.add(
this.manager.tool.$tool.listen(() => {
if (this.hasBuffer() && !this.manager.$isBusy.get()) {
this.commitBuffer();
}
})
);
}
/**
* Renders the buffer object. If the buffer renderer does not exist, it will be created and its Konva group added to the
* parent entity's buffer object group.
* @returns A promise that resolves to a boolean, indicating if the object was rendered.
*/
renderBufferObject = async (): Promise<boolean> => {
let didRender = false;
if (!this.state) {
return false;
}
if (this.state.type === 'brush_line') {
assert(this.renderer instanceof CanvasObjectBrushLine || !this.renderer);
if (!this.renderer) {
this.renderer = new CanvasObjectBrushLine(this.state, this);
this.konva.group.add(this.renderer.konva.group);
}
didRender = this.renderer.update(this.state, true);
} else if (this.state.type === 'eraser_line') {
assert(this.renderer instanceof CanvasObjectEraserLine || !this.renderer);
if (!this.renderer) {
this.renderer = new CanvasObjectEraserLine(this.state, this);
this.konva.group.add(this.renderer.konva.group);
}
didRender = this.renderer.update(this.state, true);
} else if (this.state.type === 'rect') {
assert(this.renderer instanceof CanvasObjectRect || !this.renderer);
if (!this.renderer) {
this.renderer = new CanvasObjectRect(this.state, this);
this.konva.group.add(this.renderer.konva.group);
}
didRender = this.renderer.update(this.state, true);
} else if (this.state.type === 'image') {
assert(this.renderer instanceof CanvasObjectImage || !this.renderer);
if (!this.renderer) {
this.renderer = new CanvasObjectImage(this.state, this);
this.konva.group.add(this.renderer.konva.group);
}
didRender = await this.renderer.update(this.state, true);
}
return didRender;
};
/**
* Determines if the renderer has a buffer object to render.
* @returns Whether the renderer has a buffer object to render.
*/
hasBuffer = (): boolean => {
return this.state !== null || this.renderer !== null;
};
/**
* Sets the buffer object state to render.
* @param objectState The object state to set as the buffer.
* @param resetBufferOffset Whether to reset the buffer's offset to 0,0. This is necessary when previewing filters.
* When previewing a filter, the buffer object is an image of the same size as the entity, so it should be rendered
* at the top-left corner of the entity.
* @returns A promise that resolves to a boolean, indicating if the object was rendered.
*/
setBuffer = async (objectState: AnyObjectState, resetBufferOffset: boolean = false): Promise<boolean> => {
this.log.trace('Setting buffer');
this.state = objectState;
if (resetBufferOffset) {
this.konva.group.offset({ x: 0, y: 0 });
}
return await this.renderBufferObject();
};
/**
* Clears the buffer object state.
*/
clearBuffer = () => {
if (this.state || this.renderer) {
this.log.trace('Clearing buffer');
this.renderer?.destroy();
this.renderer = null;
this.state = null;
}
};
/**
* Commits the current buffer object, pushing the buffer object state back to the application state.
*/
commitBuffer = (options?: { pushToState?: boolean }) => {
const { pushToState } = { ...options, pushToState: true };
if (!this.state || !this.renderer) {
this.log.trace('No buffer to commit');
return;
}
this.log.trace('Committing buffer');
// Move the buffer to the persistent objects group/renderers
this.parent.renderer.adoptObjectRenderer(this.renderer);
if (pushToState) {
const entityIdentifier = this.parent.entityIdentifier;
if (this.state.type === 'brush_line') {
this.manager.stateApi.addBrushLine({ entityIdentifier, brushLine: this.state });
} else if (this.state.type === 'eraser_line') {
this.manager.stateApi.addEraserLine({ entityIdentifier, eraserLine: this.state });
} else if (this.state.type === 'rect') {
this.manager.stateApi.addRect({ entityIdentifier, rect: this.state });
} else {
this.log.warn({ buffer: this.state }, 'Invalid buffer object type');
}
}
this.renderer = null;
this.state = null;
};
destroy = () => {
this.log.debug('Destroying module');
this.subscriptions.forEach((unsubscribe) => unsubscribe());
this.subscriptions.clear();
if (this.renderer) {
this.renderer.destroy();
}
};
repr = () => {
return {
id: this.id,
type: this.type,
path: this.path,
parent: this.parent.id,
bufferState: deepClone(this.state),
bufferRenderer: this.renderer?.repr(),
};
};
}

View File

@@ -96,7 +96,7 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
this.imageState = imageDTOToImageObject(imageDTO);
await this.parent.renderer.setBuffer(this.imageState, true);
await this.parent.bufferRenderer.setBuffer(this.imageState, true);
this.parent.renderer.hideObjects();
this.$isProcessing.set(false);
@@ -120,7 +120,7 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
return;
}
this.log.trace('Applying filter');
this.parent.renderer.commitBuffer();
this.parent.bufferRenderer.commitBuffer();
const rect = this.parent.transformer.getRelativeRect();
this.manager.stateApi.rasterizeEntity({
entityIdentifier: this.parent.entityIdentifier,
@@ -142,7 +142,7 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
cancelFilter = () => {
this.log.trace('Cancelling filter');
this.parent.renderer.clearBuffer();
this.parent.bufferRenderer.clearBuffer();
this.parent.renderer.showObjects();
this.parent.transformer.updatePosition();
this.parent.renderer.syncCache(true);

View File

@@ -68,19 +68,6 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
*/
subscriptions: Set<() => void> = new Set();
/**
* A buffer object state that is rendered separately from the other objects. This is used for objects that are being
* drawn in real-time, such as brush lines. The buffer object state only exists in this renderer and is not part of
* the application state until it is committed.
*/
bufferState: AnyObjectState | null = null;
/**
* The object renderer for the buffer object state. It is created when the buffer object state is set and destroyed
* when the buffer object state is cleared. This is separate from the other object renderers to allow the buffer to
* be rendered separately.
*/
bufferRenderer: AnyObjectRenderer | null = null;
/**
* A map of object renderers, keyed by their ID.
*
@@ -96,10 +83,6 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
* A Konva Group that holds all the object renderers.
*/
objectGroup: Konva.Group;
/**
* A Konva Group that holds the buffer object renderer.
*/
bufferGroup: Konva.Group;
/**
* The compositing rect is used to draw the inpaint mask as a single shape with a given opacity.
*
@@ -143,12 +126,10 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
this.konva = {
objectGroup: new Konva.Group({ name: `${this.type}:object_group`, listening: false }),
bufferGroup: new Konva.Group({ name: `${this.type}:buffer_group`, listening: false }),
compositing: null,
};
this.parent.konva.layer.add(this.konva.objectGroup);
this.parent.konva.layer.add(this.konva.bufferGroup);
if (
this.parent.entityIdentifier.type === 'inpaint_mask' ||
@@ -170,17 +151,6 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
this.parent.konva.layer.add(this.konva.compositing.group);
}
// When switching tool, commit the buffer. This is necessary to prevent the buffer from being lost when the
// user switches tool mid-drawing, for example by pressing space to pan the stage. It's easy to press space
// to pan _before_ releasing the mouse button, which would cause the buffer to be lost if we didn't commit it.
this.subscriptions.add(
this.manager.tool.$tool.listen(() => {
if (this.hasBuffer() && !this.manager.$isBusy.get()) {
this.commitBuffer();
}
})
);
// The compositing rect must cover the whole stage at all times. When the stage is scaled, moved or resized, we
// need to update the compositing rect to match the stage.
this.subscriptions.add(
@@ -227,6 +197,11 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
return didRender;
};
adoptObjectRenderer = (renderer: AnyObjectRenderer) => {
this.renderers.set(renderer.id, renderer);
renderer.konva.group.moveTo(this.konva.objectGroup);
};
syncCache = (force: boolean = false) => {
if (this.renderers.size === 0) {
this.log.trace('Clearing object group cache');
@@ -234,7 +209,7 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
} else if (force || !this.konva.objectGroup.isCached()) {
this.log.trace('Caching object group');
this.konva.objectGroup.clearCache();
this.konva.objectGroup.cache({ pixelRatio: 1 });
this.konva.objectGroup.cache({ pixelRatio: 1, imageSmoothingEnabled: false });
this.parent.renderer.updatePreviewCanvas();
}
};
@@ -295,8 +270,8 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
this.konva.compositing.group.opacity(opacity);
} else {
this.konva.objectGroup.opacity(opacity);
this.konva.bufferGroup.opacity(opacity);
}
this.parent.bufferRenderer.konva.group.opacity(opacity);
};
/**
@@ -363,131 +338,6 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
return didRender;
};
/**
* Renders the buffer object. If the buffer renderer does not exist, it will be created and its Konva group added to the
* parent entity's buffer object group.
* @returns A promise that resolves to a boolean, indicating if the object was rendered.
*/
renderBufferObject = async (): Promise<boolean> => {
let didRender = false;
if (!this.bufferState) {
return false;
}
if (this.bufferState.type === 'brush_line') {
assert(this.bufferRenderer instanceof CanvasObjectBrushLine || !this.bufferRenderer);
if (!this.bufferRenderer) {
this.bufferRenderer = new CanvasObjectBrushLine(this.bufferState, this);
this.konva.bufferGroup.add(this.bufferRenderer.konva.group);
}
didRender = this.bufferRenderer.update(this.bufferState, true);
} else if (this.bufferState.type === 'eraser_line') {
assert(this.bufferRenderer instanceof CanvasObjectEraserLine || !this.bufferRenderer);
if (!this.bufferRenderer) {
this.bufferRenderer = new CanvasObjectEraserLine(this.bufferState, this);
this.konva.bufferGroup.add(this.bufferRenderer.konva.group);
}
didRender = this.bufferRenderer.update(this.bufferState, true);
} else if (this.bufferState.type === 'rect') {
assert(this.bufferRenderer instanceof CanvasObjectRect || !this.bufferRenderer);
if (!this.bufferRenderer) {
this.bufferRenderer = new CanvasObjectRect(this.bufferState, this);
this.konva.bufferGroup.add(this.bufferRenderer.konva.group);
}
didRender = this.bufferRenderer.update(this.bufferState, true);
} else if (this.bufferState.type === 'image') {
assert(this.bufferRenderer instanceof CanvasObjectImage || !this.bufferRenderer);
if (!this.bufferRenderer) {
this.bufferRenderer = new CanvasObjectImage(this.bufferState, this);
this.konva.bufferGroup.add(this.bufferRenderer.konva.group);
}
didRender = await this.bufferRenderer.update(this.bufferState, true);
}
return didRender;
};
/**
* Determines if the renderer has a buffer object to render.
* @returns Whether the renderer has a buffer object to render.
*/
hasBuffer = (): boolean => {
return this.bufferState !== null || this.bufferRenderer !== null;
};
/**
* Sets the buffer object state to render.
* @param objectState The object state to set as the buffer.
* @param resetBufferOffset Whether to reset the buffer's offset to 0,0. This is necessary when previewing filters.
* When previewing a filter, the buffer object is an image of the same size as the entity, so it should be rendered
* at the top-left corner of the entity.
* @returns A promise that resolves to a boolean, indicating if the object was rendered.
*/
setBuffer = async (objectState: AnyObjectState, resetBufferOffset: boolean = false): Promise<boolean> => {
this.log.trace('Setting buffer');
this.bufferState = objectState;
if (resetBufferOffset) {
this.konva.bufferGroup.offset({ x: 0, y: 0 });
}
return await this.renderBufferObject();
};
/**
* Clears the buffer object state.
*/
clearBuffer = () => {
if (this.bufferState || this.bufferRenderer) {
this.log.trace('Clearing buffer');
this.bufferRenderer?.destroy();
this.bufferRenderer = null;
this.bufferState = null;
this.syncCache(true);
}
};
/**
* Commits the current buffer object, pushing the buffer object state back to the application state.
*/
commitBuffer = (options?: { pushToState?: boolean }) => {
const { pushToState } = { ...options, pushToState: true };
if (!this.bufferState || !this.bufferRenderer) {
this.log.trace('No buffer to commit');
return;
}
this.log.trace('Committing buffer');
// Move the buffer to the persistent objects group/renderers
this.bufferRenderer.konva.group.moveTo(this.konva.objectGroup);
this.renderers.set(this.bufferState.id, this.bufferRenderer);
if (pushToState) {
const entityIdentifier = this.parent.entityIdentifier;
if (this.bufferState.type === 'brush_line') {
this.manager.stateApi.addBrushLine({ entityIdentifier, brushLine: this.bufferState });
} else if (this.bufferState.type === 'eraser_line') {
this.manager.stateApi.addEraserLine({ entityIdentifier, eraserLine: this.bufferState });
} else if (this.bufferState.type === 'rect') {
this.manager.stateApi.addRect({ entityIdentifier, rect: this.bufferState });
} else {
this.log.warn({ buffer: this.bufferState }, 'Invalid buffer object type');
}
}
this.bufferRenderer = null;
this.bufferState = null;
};
hideObjects = (except: string[] = []) => {
for (const renderer of this.renderers.values()) {
renderer.setVisibility(except.includes(renderer.id));
@@ -529,7 +379,7 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
* @returns Whether the renderer has any objects to render.
*/
hasObjects = (): boolean => {
return this.renderers.size > 0 || this.bufferState !== null || this.bufferRenderer !== null;
return this.renderers.size > 0 || this.parent.bufferRenderer.hasBuffer();
};
/**
@@ -576,8 +426,8 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
});
const imageObject = imageDTOToImageObject(imageDTO);
if (replaceObjects) {
await this.setBuffer(imageObject);
this.commitBuffer({ pushToState: false });
await this.parent.bufferRenderer.setBuffer(imageObject);
this.parent.bufferRenderer.commitBuffer({ pushToState: false });
}
this.manager.stateApi.rasterizeEntity({
entityIdentifier: this.parent.entityIdentifier,
@@ -667,7 +517,6 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
path: this.path,
parent: this.parent.id,
renderers: Array.from(this.renderers.values()).map((renderer) => renderer.repr()),
buffer: this.bufferRenderer?.repr(),
};
};
}

View File

@@ -587,7 +587,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase {
rotation: 0,
};
this.parent.renderer.konva.objectGroup.setAttrs(attrs);
this.parent.renderer.konva.bufferGroup.setAttrs(attrs);
this.parent.bufferRenderer.konva.group.setAttrs(attrs);
this.konva.outlineRect.setAttrs(attrs);
this.konva.proxyRect.setAttrs(attrs);
};
@@ -608,7 +608,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase {
offsetY: pixelRect.y,
};
this.parent.renderer.konva.objectGroup.setAttrs(groupAttrs);
this.parent.renderer.konva.bufferGroup.setAttrs(groupAttrs);
this.parent.bufferRenderer.konva.group.setAttrs(groupAttrs);
this.update(position, pixelRect);
};
@@ -669,7 +669,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase {
offsetY: pixelRect.y,
};
this.parent.renderer.konva.objectGroup.setAttrs(groupAttrs);
this.parent.renderer.konva.bufferGroup.setAttrs(groupAttrs);
this.parent.bufferRenderer.konva.group.setAttrs(groupAttrs);
}
this.parent.renderer.updatePreviewCanvas();

View File

@@ -1,5 +1,6 @@
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import { deepClone } from 'common/util/deepClone';
import type { CanvasEntityBufferObjectRenderer } from 'features/controlLayers/konva/CanvasEntityBufferObjectRenderer';
import type { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntityObjectRenderer';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
@@ -11,7 +12,7 @@ export class CanvasObjectBrushLine extends CanvasModuleBase {
readonly type = 'object_brush_line';
readonly id: string;
readonly path: string[];
readonly parent: CanvasEntityObjectRenderer;
readonly parent: CanvasEntityObjectRenderer | CanvasEntityBufferObjectRenderer;
readonly manager: CanvasManager;
readonly log: Logger;
@@ -21,7 +22,7 @@ export class CanvasObjectBrushLine extends CanvasModuleBase {
line: Konva.Line;
};
constructor(state: CanvasBrushLineState, parent: CanvasEntityObjectRenderer) {
constructor(state: CanvasBrushLineState, parent: CanvasEntityObjectRenderer | CanvasEntityBufferObjectRenderer) {
super();
const { id, clip } = state;
this.id = id;

View File

@@ -1,4 +1,5 @@
import { deepClone } from 'common/util/deepClone';
import type { CanvasEntityBufferObjectRenderer } from 'features/controlLayers/konva/CanvasEntityBufferObjectRenderer';
import type { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntityObjectRenderer';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
@@ -10,7 +11,7 @@ export class CanvasObjectEraserLine extends CanvasModuleBase {
readonly type = 'object_eraser_line';
readonly id: string;
readonly path: string[];
readonly parent: CanvasEntityObjectRenderer;
readonly parent: CanvasEntityObjectRenderer | CanvasEntityBufferObjectRenderer;
readonly manager: CanvasManager;
readonly log: Logger;
@@ -20,7 +21,7 @@ export class CanvasObjectEraserLine extends CanvasModuleBase {
line: Konva.Line;
};
constructor(state: CanvasEraserLineState, parent: CanvasEntityObjectRenderer) {
constructor(state: CanvasEraserLineState, parent: CanvasEntityObjectRenderer | CanvasEntityBufferObjectRenderer) {
super();
this.id = state.id;
this.parent = parent;

View File

@@ -1,5 +1,6 @@
import { Mutex } from 'async-mutex';
import { deepClone } from 'common/util/deepClone';
import type { CanvasEntityBufferObjectRenderer } from 'features/controlLayers/konva/CanvasEntityBufferObjectRenderer';
import type { CanvasEntityFilterer } from 'features/controlLayers/konva/CanvasEntityFilterer';
import type { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntityObjectRenderer';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
@@ -16,7 +17,11 @@ export class CanvasObjectImage extends CanvasModuleBase {
readonly type = 'object_image';
readonly id: string;
readonly path: string[];
readonly parent: CanvasEntityObjectRenderer | CanvasStagingAreaModule | CanvasEntityFilterer;
readonly parent:
| CanvasEntityObjectRenderer
| CanvasEntityBufferObjectRenderer
| CanvasStagingAreaModule
| CanvasEntityFilterer;
readonly manager: CanvasManager;
readonly log: Logger;
@@ -33,7 +38,11 @@ export class CanvasObjectImage extends CanvasModuleBase {
constructor(
state: CanvasImageState,
parent: CanvasEntityObjectRenderer | CanvasStagingAreaModule | CanvasEntityFilterer
parent:
| CanvasEntityObjectRenderer
| CanvasEntityBufferObjectRenderer
| CanvasStagingAreaModule
| CanvasEntityFilterer
) {
super();
this.id = state.id;

View File

@@ -1,5 +1,6 @@
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import { deepClone } from 'common/util/deepClone';
import type { CanvasEntityBufferObjectRenderer } from 'features/controlLayers/konva/CanvasEntityBufferObjectRenderer';
import type { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntityObjectRenderer';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
@@ -11,7 +12,7 @@ export class CanvasObjectRect extends CanvasModuleBase {
readonly type = 'object_rect';
readonly id: string;
readonly path: string[];
readonly parent: CanvasEntityObjectRenderer;
readonly parent: CanvasEntityObjectRenderer | CanvasEntityBufferObjectRenderer;
readonly manager: CanvasManager;
readonly log: Logger;
@@ -22,7 +23,7 @@ export class CanvasObjectRect extends CanvasModuleBase {
};
isFirstRender: boolean = false;
constructor(state: CanvasRectState, parent: CanvasEntityObjectRenderer) {
constructor(state: CanvasRectState, parent: CanvasEntityObjectRenderer | CanvasEntityBufferObjectRenderer) {
super();
this.id = state.id;
this.parent = parent;

View File

@@ -307,15 +307,15 @@ export class CanvasToolModule extends CanvasModuleBase {
return;
}
if (selectedEntity.renderer.bufferState?.type !== 'rect' && selectedEntity.renderer.hasBuffer()) {
selectedEntity.renderer.commitBuffer();
if (selectedEntity.bufferRenderer.state?.type !== 'rect' && selectedEntity.bufferRenderer.hasBuffer()) {
selectedEntity.bufferRenderer.commitBuffer();
return;
}
if (tool === 'brush') {
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
await selectedEntity.renderer.setBuffer({
await selectedEntity.bufferRenderer.setBuffer({
id: getPrefixedId('brush_line'),
type: 'brush_line',
points: [alignedPoint.x, alignedPoint.y],
@@ -329,10 +329,10 @@ export class CanvasToolModule extends CanvasModuleBase {
if (tool === 'eraser') {
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
if (selectedEntity.renderer.bufferState && selectedEntity.renderer.hasBuffer()) {
selectedEntity.renderer.commitBuffer();
if (selectedEntity.bufferRenderer.state && selectedEntity.bufferRenderer.hasBuffer()) {
selectedEntity.bufferRenderer.commitBuffer();
}
await selectedEntity.renderer.setBuffer({
await selectedEntity.bufferRenderer.setBuffer({
id: getPrefixedId('eraser_line'),
type: 'eraser_line',
points: [alignedPoint.x, alignedPoint.y],
@@ -380,11 +380,11 @@ export class CanvasToolModule extends CanvasModuleBase {
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
if (e.evt.shiftKey && lastLinePoint) {
// Create a straight line from the last line point
if (selectedEntity.renderer.hasBuffer()) {
selectedEntity.renderer.commitBuffer();
if (selectedEntity.bufferRenderer.hasBuffer()) {
selectedEntity.bufferRenderer.commitBuffer();
}
await selectedEntity.renderer.setBuffer({
await selectedEntity.bufferRenderer.setBuffer({
id: getPrefixedId('brush_line'),
type: 'brush_line',
points: [
@@ -399,10 +399,10 @@ export class CanvasToolModule extends CanvasModuleBase {
clip: this.getClip(selectedEntity.state),
});
} else {
if (selectedEntity.renderer.hasBuffer()) {
selectedEntity.renderer.commitBuffer();
if (selectedEntity.bufferRenderer.hasBuffer()) {
selectedEntity.bufferRenderer.commitBuffer();
}
await selectedEntity.renderer.setBuffer({
await selectedEntity.bufferRenderer.setBuffer({
id: getPrefixedId('brush_line'),
type: 'brush_line',
points: [alignedPoint.x, alignedPoint.y],
@@ -418,10 +418,10 @@ export class CanvasToolModule extends CanvasModuleBase {
const alignedPoint = alignCoordForTool(normalizedPoint, settings.eraserWidth);
if (e.evt.shiftKey && lastLinePoint) {
// Create a straight line from the last line point
if (selectedEntity.renderer.hasBuffer()) {
selectedEntity.renderer.commitBuffer();
if (selectedEntity.bufferRenderer.hasBuffer()) {
selectedEntity.bufferRenderer.commitBuffer();
}
await selectedEntity.renderer.setBuffer({
await selectedEntity.bufferRenderer.setBuffer({
id: getPrefixedId('eraser_line'),
type: 'eraser_line',
points: [
@@ -435,10 +435,10 @@ export class CanvasToolModule extends CanvasModuleBase {
clip: this.getClip(selectedEntity.state),
});
} else {
if (selectedEntity.renderer.hasBuffer()) {
selectedEntity.renderer.commitBuffer();
if (selectedEntity.bufferRenderer.hasBuffer()) {
selectedEntity.bufferRenderer.commitBuffer();
}
await selectedEntity.renderer.setBuffer({
await selectedEntity.bufferRenderer.setBuffer({
id: getPrefixedId('eraser_line'),
type: 'eraser_line',
points: [alignedPoint.x, alignedPoint.y],
@@ -449,10 +449,10 @@ export class CanvasToolModule extends CanvasModuleBase {
}
if (tool === 'rect') {
if (selectedEntity.renderer.hasBuffer()) {
selectedEntity.renderer.commitBuffer();
if (selectedEntity.bufferRenderer.hasBuffer()) {
selectedEntity.bufferRenderer.commitBuffer();
}
await selectedEntity.renderer.setBuffer({
await selectedEntity.bufferRenderer.setBuffer({
id: getPrefixedId('rect'),
type: 'rect',
rect: { x: Math.round(normalizedPoint.x), y: Math.round(normalizedPoint.y), width: 0, height: 0 },
@@ -483,26 +483,32 @@ export class CanvasToolModule extends CanvasModuleBase {
const tool = this.$tool.get();
if (tool === 'brush') {
if (selectedEntity.renderer.bufferState?.type === 'brush_line' && selectedEntity.renderer.hasBuffer()) {
selectedEntity.renderer.commitBuffer();
if (
selectedEntity.bufferRenderer.state?.type === 'brush_line' &&
selectedEntity.bufferRenderer.hasBuffer()
) {
selectedEntity.bufferRenderer.commitBuffer();
} else {
selectedEntity.renderer.clearBuffer();
selectedEntity.bufferRenderer.clearBuffer();
}
}
if (tool === 'eraser') {
if (selectedEntity.renderer.bufferState?.type === 'eraser_line' && selectedEntity.renderer.hasBuffer()) {
selectedEntity.renderer.commitBuffer();
if (
selectedEntity.bufferRenderer.state?.type === 'eraser_line' &&
selectedEntity.bufferRenderer.hasBuffer()
) {
selectedEntity.bufferRenderer.commitBuffer();
} else {
selectedEntity.renderer.clearBuffer();
selectedEntity.bufferRenderer.clearBuffer();
}
}
if (tool === 'rect') {
if (selectedEntity.renderer.bufferState?.type === 'rect' && selectedEntity.renderer.hasBuffer()) {
selectedEntity.renderer.commitBuffer();
if (selectedEntity.bufferRenderer.state?.type === 'rect' && selectedEntity.bufferRenderer.hasBuffer()) {
selectedEntity.bufferRenderer.commitBuffer();
} else {
selectedEntity.renderer.clearBuffer();
selectedEntity.bufferRenderer.clearBuffer();
}
}
} finally {
@@ -535,7 +541,7 @@ export class CanvasToolModule extends CanvasModuleBase {
return;
}
const bufferState = selectedEntity.renderer.bufferState;
const bufferState = selectedEntity.bufferRenderer.state;
if (!bufferState) {
return;
@@ -559,7 +565,7 @@ export class CanvasToolModule extends CanvasModuleBase {
}
bufferState.points.push(alignedPoint.x, alignedPoint.y);
await selectedEntity.renderer.setBuffer(bufferState);
await selectedEntity.bufferRenderer.setBuffer(bufferState);
} else if (tool === 'eraser' && bufferState.type === 'eraser_line') {
const lastPoint = getLastPointOfLine(bufferState.points);
const minDistance = settings.eraserWidth * this.config.BRUSH_SPACING_TARGET_SCALE;
@@ -576,15 +582,15 @@ export class CanvasToolModule extends CanvasModuleBase {
}
bufferState.points.push(alignedPoint.x, alignedPoint.y);
await selectedEntity.renderer.setBuffer(bufferState);
await selectedEntity.bufferRenderer.setBuffer(bufferState);
} else if (tool === 'rect' && bufferState.type === 'rect') {
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
const alignedPoint = floorCoord(normalizedPoint);
bufferState.rect.width = Math.round(alignedPoint.x - bufferState.rect.x);
bufferState.rect.height = Math.round(alignedPoint.y - bufferState.rect.y);
await selectedEntity.renderer.setBuffer(bufferState);
await selectedEntity.bufferRenderer.setBuffer(bufferState);
} else {
selectedEntity?.renderer.clearBuffer();
selectedEntity?.bufferRenderer.clearBuffer();
}
} finally {
this.render();
@@ -599,8 +605,12 @@ export class CanvasToolModule extends CanvasModuleBase {
this.$cursorPos.set(null);
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
if (selectedEntity && selectedEntity.renderer.bufferState?.type !== 'rect' && selectedEntity.renderer.hasBuffer()) {
selectedEntity.renderer.commitBuffer();
if (
selectedEntity &&
selectedEntity.bufferRenderer.state?.type !== 'rect' &&
selectedEntity.bufferRenderer.hasBuffer()
) {
selectedEntity.bufferRenderer.commitBuffer();
}
this.render();
@@ -640,8 +650,8 @@ export class CanvasToolModule extends CanvasModuleBase {
this.$isMouseDown.set(false);
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
if (selectedEntity && selectedEntity.renderer.hasBuffer() && !this.manager.$isBusy.get()) {
selectedEntity.renderer.commitBuffer();
if (selectedEntity && selectedEntity.bufferRenderer.hasBuffer() && !this.manager.$isBusy.get()) {
selectedEntity.bufferRenderer.commitBuffer();
}
};
@@ -659,7 +669,7 @@ export class CanvasToolModule extends CanvasModuleBase {
e.preventDefault();
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
if (selectedEntity) {
selectedEntity.renderer.clearBuffer();
selectedEntity.bufferRenderer.clearBuffer();
}
return;
}