feat(ui): split out object renderer

This commit is contained in:
psychedelicious 2024-08-02 18:35:31 +10:00
parent 136ffd97ca
commit 1095b7c37f
16 changed files with 444 additions and 358 deletions

View File

@ -1,19 +1,19 @@
import type { JSONObject } from 'common/types';
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import { deepClone } from 'common/util/deepClone';
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
import type { CanvasBrushLineState } from 'features/controlLayers/store/types';
import Konva from 'konva';
import type { Logger } from 'roarr';
export class CanvasBrushLine {
export class CanvasBrushLineRenderer {
static TYPE = 'brush_line';
static GROUP_NAME = `${CanvasBrushLine.TYPE}_group`;
static LINE_NAME = `${CanvasBrushLine.TYPE}_line`;
static GROUP_NAME = `${CanvasBrushLineRenderer.TYPE}_group`;
static LINE_NAME = `${CanvasBrushLineRenderer.TYPE}_line`;
id: string;
parent: CanvasLayer;
parent: CanvasObjectRenderer;
manager: CanvasManager;
log: Logger;
getLoggingContext: (extra?: JSONObject) => JSONObject;
@ -23,8 +23,9 @@ export class CanvasBrushLine {
group: Konva.Group;
line: Konva.Line;
};
isFirstRender: boolean = false;
constructor(state: CanvasBrushLineState, parent: CanvasLayer) {
constructor(state: CanvasBrushLineState, parent: CanvasObjectRenderer) {
const { id, strokeWidth, clip, color, points } = state;
this.id = id;
this.parent = parent;
@ -37,12 +38,12 @@ export class CanvasBrushLine {
this.konva = {
group: new Konva.Group({
name: CanvasBrushLine.GROUP_NAME,
name: CanvasBrushLineRenderer.GROUP_NAME,
clip,
listening: false,
}),
line: new Konva.Line({
name: CanvasBrushLine.LINE_NAME,
name: CanvasBrushLineRenderer.LINE_NAME,
listening: false,
shadowForStrokeEnabled: false,
strokeWidth,
@ -59,8 +60,10 @@ export class CanvasBrushLine {
this.state = state;
}
update(state: CanvasBrushLineState, force?: boolean): boolean {
update(state: CanvasBrushLineState, force = this.isFirstRender): boolean {
if (force || this.state !== state) {
this.isFirstRender = false;
this.log.trace({ state }, 'Updating brush line');
const { points, color, clip, strokeWidth } = state;
this.konva.line.setAttrs({
@ -72,9 +75,9 @@ export class CanvasBrushLine {
});
this.state = state;
return true;
} else {
return false;
}
return false;
}
destroy() {
@ -90,8 +93,9 @@ export class CanvasBrushLine {
repr() {
return {
id: this.id,
type: CanvasBrushLine.TYPE,
type: CanvasBrushLineRenderer.TYPE,
parent: this.parent.id,
isFirstRender: this.isFirstRender,
state: deepClone(this.state),
};
}

View File

@ -1,5 +1,5 @@
import { CanvasEntity } from 'features/controlLayers/konva/CanvasEntity';
import { CanvasImage } from 'features/controlLayers/konva/CanvasImage';
import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer';
import { type CanvasControlAdapterState, isDrawingTool } from 'features/controlLayers/store/types';
@ -21,7 +21,7 @@ export class CanvasControlAdapter extends CanvasEntity {
objectGroup: Konva.Group;
};
image: CanvasImage | null;
image: CanvasImageRenderer | null;
transformer: CanvasTransformer;
constructor(state: CanvasControlAdapterState, manager: CanvasManager) {
@ -68,7 +68,7 @@ export class CanvasControlAdapter extends CanvasEntity {
didDraw = true;
}
} else if (!this.image) {
this.image = new CanvasImage(imageObject, this);
this.image = new CanvasImageRenderer(imageObject, this);
this.updateGroup(true);
this.konva.objectGroup.add(this.image.konva.group);
await this.image.updateImageSource(imageObject.image.name);

View File

@ -1,30 +1,31 @@
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import { deepClone } from 'common/util/deepClone';
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
import type { CanvasEraserLineState, GetLoggingContext } from 'features/controlLayers/store/types';
import { RGBA_RED } from 'features/controlLayers/store/types';
import Konva from 'konva';
import type { Logger } from 'roarr';
export class CanvasEraserLine {
export class CanvasEraserLineRenderer {
static TYPE = 'eraser_line';
static GROUP_NAME = `${CanvasEraserLine.TYPE}_group`;
static LINE_NAME = `${CanvasEraserLine.TYPE}_line`;
static GROUP_NAME = `${CanvasEraserLineRenderer.TYPE}_group`;
static LINE_NAME = `${CanvasEraserLineRenderer.TYPE}_line`;
id: string;
parent: CanvasLayer;
parent: CanvasObjectRenderer;
manager: CanvasManager;
log: Logger;
getLoggingContext: GetLoggingContext;
isFirstRender: boolean = false;
state: CanvasEraserLineState;
konva: {
group: Konva.Group;
line: Konva.Line;
};
constructor(state: CanvasEraserLineState, parent: CanvasLayer) {
constructor(state: CanvasEraserLineState, parent: CanvasObjectRenderer) {
const { id, strokeWidth, clip, points } = state;
this.id = id;
this.parent = parent;
@ -36,12 +37,12 @@ export class CanvasEraserLine {
this.konva = {
group: new Konva.Group({
name: CanvasEraserLine.GROUP_NAME,
name: CanvasEraserLineRenderer.GROUP_NAME,
clip,
listening: false,
}),
line: new Konva.Line({
name: CanvasEraserLine.LINE_NAME,
name: CanvasEraserLineRenderer.LINE_NAME,
listening: false,
shadowForStrokeEnabled: false,
strokeWidth,
@ -58,8 +59,10 @@ export class CanvasEraserLine {
this.state = state;
}
update(state: CanvasEraserLineState, force?: boolean): boolean {
update(state: CanvasEraserLineState, force = this.isFirstRender): boolean {
if (force || this.state !== state) {
this.isFirstRender = false;
this.log.trace({ state }, 'Updating eraser line');
const { points, clip, strokeWidth } = state;
this.konva.line.setAttrs({
@ -70,9 +73,9 @@ export class CanvasEraserLine {
});
this.state = state;
return true;
} else {
return false;
}
return false;
}
destroy() {
@ -88,8 +91,9 @@ export class CanvasEraserLine {
repr() {
return {
id: this.id,
type: CanvasEraserLine.TYPE,
type: CanvasEraserLineRenderer.TYPE,
parent: this.parent.id,
isFirstRender: this.isFirstRender,
state: deepClone(this.state),
};
}

View File

@ -1,25 +1,24 @@
import { deepClone } from 'common/util/deepClone';
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import type { CanvasStagingArea } from 'features/controlLayers/konva/CanvasStagingArea';
import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
import { FILTER_MAP } from 'features/controlLayers/konva/filters';
import { loadImage } from 'features/controlLayers/konva/util';
import type { GetLoggingContext, CanvasImageState } from 'features/controlLayers/store/types';
import type { CanvasImageState, GetLoggingContext } from 'features/controlLayers/store/types';
import { t } from 'i18next';
import Konva from 'konva';
import type { Logger } from 'roarr';
import { getImageDTO } from 'services/api/endpoints/images';
export class CanvasImage {
export class CanvasImageRenderer {
static TYPE = 'image';
static GROUP_NAME = `${CanvasImage.TYPE}_group`;
static IMAGE_NAME = `${CanvasImage.TYPE}_image`;
static PLACEHOLDER_GROUP_NAME = `${CanvasImage.TYPE}_placeholder-group`;
static PLACEHOLDER_RECT_NAME = `${CanvasImage.TYPE}_placeholder-rect`;
static PLACEHOLDER_TEXT_NAME = `${CanvasImage.TYPE}_placeholder-text`;
static GROUP_NAME = `${CanvasImageRenderer.TYPE}_group`;
static IMAGE_NAME = `${CanvasImageRenderer.TYPE}_image`;
static PLACEHOLDER_GROUP_NAME = `${CanvasImageRenderer.TYPE}_placeholder-group`;
static PLACEHOLDER_RECT_NAME = `${CanvasImageRenderer.TYPE}_placeholder-rect`;
static PLACEHOLDER_TEXT_NAME = `${CanvasImageRenderer.TYPE}_placeholder-text`;
id: string;
parent: CanvasLayer | CanvasStagingArea;
parent: CanvasObjectRenderer;
manager: CanvasManager;
log: Logger;
getLoggingContext: GetLoggingContext;
@ -33,8 +32,9 @@ export class CanvasImage {
imageName: string | null;
isLoading: boolean;
isError: boolean;
isFirstRender: boolean = true;
constructor(state: CanvasImageState, parent: CanvasLayer | CanvasStagingArea) {
constructor(state: CanvasImageState, parent: CanvasObjectRenderer) {
const { id, width, height, x, y } = state;
this.id = id;
this.parent = parent;
@ -45,18 +45,18 @@ export class CanvasImage {
this.log.trace({ state }, 'Creating image');
this.konva = {
group: new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }),
group: new Konva.Group({ name: CanvasImageRenderer.GROUP_NAME, listening: false, x, y }),
placeholder: {
group: new Konva.Group({ name: CanvasImage.PLACEHOLDER_GROUP_NAME, listening: false }),
group: new Konva.Group({ name: CanvasImageRenderer.PLACEHOLDER_GROUP_NAME, listening: false }),
rect: new Konva.Rect({
name: CanvasImage.PLACEHOLDER_RECT_NAME,
name: CanvasImageRenderer.PLACEHOLDER_RECT_NAME,
fill: 'hsl(220 12% 45% / 1)', // 'base.500'
width,
height,
listening: false,
}),
text: new Konva.Text({
name: CanvasImage.PLACEHOLDER_TEXT_NAME,
name: CanvasImageRenderer.PLACEHOLDER_TEXT_NAME,
fill: 'hsl(220 12% 10% / 1)', // 'base.900'
width,
height,
@ -81,7 +81,7 @@ export class CanvasImage {
this.state = state;
}
async updateImageSource(imageName: string) {
updateImageSource = async (imageName: string) => {
try {
this.log.trace({ imageName }, 'Updating image source');
@ -106,7 +106,7 @@ export class CanvasImage {
});
} else {
this.konva.image = new Konva.Image({
name: CanvasImage.IMAGE_NAME,
name: CanvasImageRenderer.IMAGE_NAME,
listening: false,
image: imageEl,
width: this.state.width,
@ -136,14 +136,16 @@ export class CanvasImage {
this.konva.placeholder.text.text(t('common.imageFailedToLoad', 'Image Failed to Load'));
this.konva.placeholder.group.visible(true);
}
}
};
update = async (state: CanvasImageState, force = this.isFirstRender): Promise<boolean> => {
if (force || this.state !== state) {
this.isFirstRender = false;
async update(state: CanvasImageState, force?: boolean): Promise<boolean> {
if (this.state !== state || force) {
this.log.trace({ state }, 'Updating image');
const { width, height, x, y, image, filters } = state;
if (this.state.image.name !== image.name || force) {
if (force || (this.state.image.name !== image.name && !this.isLoading)) {
await this.updateImageSource(image.name);
}
this.konva.image?.setAttrs({ x, y, width, height });
@ -158,30 +160,31 @@ export class CanvasImage {
this.konva.placeholder.text.setAttrs({ width, height, fontSize: width / 16 });
this.state = state;
return true;
} else {
return false;
}
}
destroy() {
return false;
};
destroy = () => {
this.log.trace('Destroying image');
this.konva.group.destroy();
}
};
setVisibility(isVisible: boolean): void {
setVisibility = (isVisible: boolean): void => {
this.log.trace({ isVisible }, 'Setting image visibility');
this.konva.group.visible(isVisible);
}
};
repr() {
repr = () => {
return {
id: this.id,
type: CanvasImage.TYPE,
type: CanvasImageRenderer.TYPE,
parent: this.parent.id,
imageName: this.imageName,
isLoading: this.isLoading,
isError: this.isError,
isFirstRender: this.isFirstRender,
state: deepClone(this.state),
};
}
};
}

View File

@ -1,4 +1,4 @@
import { CanvasImage } from 'features/controlLayers/konva/CanvasImage';
import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import type { InitialImageEntity } from 'features/controlLayers/store/types';
import Konva from 'konva';
@ -20,7 +20,7 @@ export class CanvasInitialImage {
objectGroup: Konva.Group;
};
image: CanvasImage | null;
image: CanvasImageRenderer | null;
constructor(state: InitialImageEntity, manager: CanvasManager) {
this.manager = manager;
@ -45,7 +45,7 @@ export class CanvasInitialImage {
}
if (!this.image) {
this.image = new CanvasImage(this.state.imageObject);
this.image = new CanvasImageRenderer(this.state.imageObject);
this.konva.objectGroup.add(this.image.konva.group);
await this.image.update(this.state.imageObject, true);
} else if (!this.image.isLoading && !this.image.isError) {

View File

@ -1,8 +1,8 @@
import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine';
import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine';
import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine';
import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasRect } from 'features/controlLayers/konva/CanvasRect';
import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect';
import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox';
import { mapId } from 'features/controlLayers/konva/util';
import type { CanvasBrushLineState, CanvasEraserLineState, CanvasInpaintMaskState, CanvasRectState } from 'features/controlLayers/store/types';
@ -31,7 +31,7 @@ export class CanvasInpaintMask {
transformer: Konva.Transformer;
compositingRect: Konva.Rect;
};
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect>;
objects: Map<string, CanvasBrushLineRenderer | CanvasEraserLineRenderer | CanvasRectRenderer>;
constructor(state: CanvasInpaintMaskState, manager: CanvasManager) {
this.manager = manager;
@ -156,10 +156,10 @@ export class CanvasInpaintMask {
private async renderObject(obj: CanvasInpaintMaskState['objects'][number], force = false): Promise<boolean> {
if (obj.type === 'brush_line') {
let brushLine = this.objects.get(obj.id);
assert(brushLine instanceof CanvasBrushLine || brushLine === undefined);
assert(brushLine instanceof CanvasBrushLineRenderer || brushLine === undefined);
if (!brushLine) {
brushLine = new CanvasBrushLine(obj);
brushLine = new CanvasBrushLineRenderer(obj);
this.objects.set(brushLine.id, brushLine);
this.konva.objectGroup.add(brushLine.konva.group);
return true;
@ -170,10 +170,10 @@ export class CanvasInpaintMask {
}
} else if (obj.type === 'eraser_line') {
let eraserLine = this.objects.get(obj.id);
assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined);
assert(eraserLine instanceof CanvasEraserLineRenderer || eraserLine === undefined);
if (!eraserLine) {
eraserLine = new CanvasEraserLine(obj);
eraserLine = new CanvasEraserLineRenderer(obj);
this.objects.set(eraserLine.id, eraserLine);
this.konva.objectGroup.add(eraserLine.konva.group);
return true;
@ -184,10 +184,10 @@ export class CanvasInpaintMask {
}
} else if (obj.type === 'rect') {
let rect = this.objects.get(obj.id);
assert(rect instanceof CanvasRect || rect === undefined);
assert(rect instanceof CanvasRectRenderer || rect === undefined);
if (!rect) {
rect = new CanvasRect(obj);
rect = new CanvasRectRenderer(obj);
this.objects.set(rect.id, rect);
this.konva.objectGroup.add(rect.konva.group);
return true;

View File

@ -1,18 +1,12 @@
import { getStore } from 'app/store/nanostores/store';
import { deepClone } from 'common/util/deepClone';
import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine';
import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine';
import { CanvasImage } from 'features/controlLayers/konva/CanvasImage';
import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasRect } from 'features/controlLayers/konva/CanvasRect';
import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer';
import { getPrefixedId, konvaNodeToBlob, mapId, previewBlob } from 'features/controlLayers/konva/util';
import { konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util';
import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice';
import type {
CanvasBrushLineState,
CanvasEraserLineState,
CanvasLayerState,
CanvasRectState,
CanvasV2State,
Coordinate,
GetLoggingContext,
@ -23,34 +17,28 @@ import Konva from 'konva';
import { debounce, get } from 'lodash-es';
import type { Logger } from 'roarr';
import { uploadImage } from 'services/api/endpoints/images';
import { assert } from 'tsafe';
export class CanvasLayer {
static TYPE = 'layer';
static LAYER_NAME = `${CanvasLayer.TYPE}_layer`;
static TRANSFORMER_NAME = `${CanvasLayer.TYPE}_transformer`;
static INTERACTION_RECT_NAME = `${CanvasLayer.TYPE}_interaction-rect`;
static GROUP_NAME = `${CanvasLayer.TYPE}_group`;
static OBJECT_GROUP_NAME = `${CanvasLayer.TYPE}_object-group`;
static BBOX_NAME = `${CanvasLayer.TYPE}_bbox`;
static KONVA_LAYER_NAME = `${CanvasLayer.TYPE}_layer`;
static KONVA_OBJECT_GROUP_NAME = `${CanvasLayer.TYPE}_object-group`;
id: string;
manager: CanvasManager;
log: Logger;
getLoggingContext: GetLoggingContext;
drawingBuffer: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null;
state: CanvasLayerState;
konva: {
layer: Konva.Layer;
objectGroup: Konva.Group;
};
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage>;
transformer: CanvasTransformer;
renderer: CanvasObjectRenderer;
isFirstRender: boolean = true;
bboxNeedsUpdate: boolean;
isFirstRender: boolean;
isTransforming: boolean;
isPendingBboxCalculation: boolean;
@ -67,26 +55,24 @@ export class CanvasLayer {
this.konva = {
layer: new Konva.Layer({
id: this.id,
name: CanvasLayer.LAYER_NAME,
name: CanvasLayer.KONVA_LAYER_NAME,
listening: false,
imageSmoothingEnabled: false,
}),
objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }),
objectGroup: new Konva.Group({ name: CanvasLayer.KONVA_OBJECT_GROUP_NAME, listening: false }),
};
this.transformer = new CanvasTransformer(this, this.konva.objectGroup);
this.transformer = new CanvasTransformer(this);
this.renderer = new CanvasObjectRenderer(this);
this.konva.layer.add(this.konva.objectGroup);
this.konva.layer.add(...this.transformer.getNodes());
this.objects = new Map();
this.drawingBuffer = null;
this.state = state;
this.rect = this.getDefaultRect();
this.bbox = this.getDefaultRect();
this.bboxNeedsUpdate = true;
this.isTransforming = false;
this.isFirstRender = true;
this.isPendingBboxCalculation = false;
}
@ -94,47 +80,10 @@ export class CanvasLayer {
this.log.debug('Destroying layer');
// We need to call the destroy method on all children so they can do their own cleanup.
this.transformer.destroy();
for (const obj of this.objects.values()) {
obj.destroy();
}
this.renderer.destroy();
this.konva.layer.destroy();
};
getDrawingBuffer = () => {
return this.drawingBuffer;
};
setDrawingBuffer = async (obj: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null) => {
if (obj) {
this.drawingBuffer = obj;
await this._renderObject(this.drawingBuffer, true);
} else {
this.drawingBuffer = null;
}
};
finalizeDrawingBuffer = async () => {
if (!this.drawingBuffer) {
return;
}
const drawingBuffer = this.drawingBuffer;
await 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
// a non-buffer object, and we won't trigger things like bbox calculation
if (drawingBuffer.type === 'brush_line') {
drawingBuffer.id = getPrefixedId('brush_line');
this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: drawingBuffer }, 'layer');
} else if (drawingBuffer.type === 'eraser_line') {
drawingBuffer.id = getPrefixedId('brush_line');
this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: drawingBuffer }, 'layer');
} else if (drawingBuffer.type === 'rect') {
drawingBuffer.id = getPrefixedId('brush_line');
this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: drawingBuffer }, 'layer');
}
};
update = async (arg?: { state: CanvasLayerState; toolState: CanvasV2State['tool']; isSelected: boolean }) => {
const state = get(arg, 'state', this.state);
const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState());
@ -173,8 +122,7 @@ export class CanvasLayer {
updateVisibility = (arg?: { isEnabled: boolean }) => {
this.log.trace('Updating visibility');
const isEnabled = get(arg, 'isEnabled', this.state.isEnabled);
const hasObjects = this.objects.size > 0 || this.drawingBuffer !== null;
this.konva.layer.visible(isEnabled && hasObjects);
this.konva.layer.visible(isEnabled && this.renderer.hasObjects());
};
updatePosition = (arg?: { position: Coordinate }) => {
@ -196,30 +144,7 @@ export class CanvasLayer {
const objects = get(arg, 'objects', this.state.objects);
const objectIds = objects.map(mapId);
let didUpdate = false;
// Destroy any objects that are no longer in state
for (const object of this.objects.values()) {
if (!objectIds.includes(object.id) && object.id !== this.drawingBuffer?.id) {
this.objects.delete(object.id);
object.destroy();
didUpdate = true;
}
}
for (const obj of objects) {
if (await this._renderObject(obj)) {
didUpdate = true;
}
}
if (this.drawingBuffer) {
if (await this._renderObject(this.drawingBuffer)) {
didUpdate = true;
}
}
const didUpdate = await this.renderer.render(objects);
if (didUpdate) {
this.calculateBbox();
@ -240,7 +165,7 @@ export class CanvasLayer {
const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState());
const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id));
if (this.objects.size === 0) {
if (!this.renderer.hasObjects()) {
// The layer is totally empty, we can just disable the layer
this.konva.layer.listening(false);
this.transformer.setMode('off');
@ -279,7 +204,7 @@ export class CanvasLayer {
// eraser lines, fully clipped brush lines or if it has been fully erased.
if (this.bbox.width === 0 || this.bbox.height === 0) {
// We shouldn't reset on the first render - the bbox will be calculated on the next render
if (!this.isFirstRender && this.objects.size > 0) {
if (!this.isFirstRender && !this.renderer.hasObjects()) {
// The layer is fully transparent but has objects - reset it
this.manager.stateApi.onEntityReset({ id: this.id }, 'layer');
}
@ -297,67 +222,6 @@ export class CanvasLayer {
});
};
_renderObject = async (obj: CanvasLayerState['objects'][number], force = false): Promise<boolean> => {
if (obj.type === 'brush_line') {
let brushLine = this.objects.get(obj.id);
assert(brushLine instanceof CanvasBrushLine || brushLine === undefined);
if (!brushLine) {
brushLine = new CanvasBrushLine(obj, this);
this.objects.set(brushLine.id, brushLine);
this.konva.objectGroup.add(brushLine.konva.group);
return true;
} else {
return await brushLine.update(obj, force);
}
} else if (obj.type === 'eraser_line') {
let eraserLine = this.objects.get(obj.id);
assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined);
if (!eraserLine) {
eraserLine = new CanvasEraserLine(obj, this);
this.objects.set(eraserLine.id, eraserLine);
this.konva.objectGroup.add(eraserLine.konva.group);
return true;
} else {
if (await eraserLine.update(obj, force)) {
return true;
}
}
} else if (obj.type === 'rect') {
let rect = this.objects.get(obj.id);
assert(rect instanceof CanvasRect || rect === undefined);
if (!rect) {
rect = new CanvasRect(obj, this);
this.objects.set(rect.id, rect);
this.konva.objectGroup.add(rect.konva.group);
return true;
} else {
if (await rect.update(obj, force)) {
return true;
}
}
} else if (obj.type === 'image') {
let image = this.objects.get(obj.id);
assert(image instanceof CanvasImage || image === undefined);
if (!image) {
image = new CanvasImage(obj, this);
this.objects.set(image.id, image);
this.konva.objectGroup.add(image.konva.group);
await image.updateImageSource(obj.image.name);
return true;
} else {
if (await image.update(obj, force)) {
return true;
}
}
}
return false;
};
startTransform = () => {
this.log.debug('Starting transform');
this.isTransforming = true;
@ -365,9 +229,8 @@ export class CanvasLayer {
// When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or
// interaction rect are listening, it will interrupt the stage's drag events. So we should disable listening
// when the view tool is selected
const listening = this.manager.stateApi.getToolState().selected !== 'view';
this.konva.layer.listening(listening);
const shouldListen = this.manager.stateApi.getToolState().selected !== 'view';
this.konva.layer.listening(shouldListen);
this.transformer.setMode('transform');
};
@ -395,12 +258,8 @@ export class CanvasLayer {
const imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true);
const { dispatch } = getStore();
const imageObject = imageDTOToImageObject(imageDTO);
await this._renderObject(imageObject, true);
for (const obj of this.objects.values()) {
if (obj.id !== imageObject.id) {
obj.konva.group.visible(false);
}
}
await this.renderer.renderObject(imageObject, true);
this.renderer.hideAll([imageObject.id]);
this.resetScale();
dispatch(layerRasterized({ id: this.id, imageObject, position: { x: Math.round(rect.x), y: Math.round(rect.y) } }));
};
@ -424,7 +283,7 @@ export class CanvasLayer {
this.isPendingBboxCalculation = true;
if (this.objects.size === 0) {
if (!this.renderer.hasObjects()) {
this.log.trace('No objects, resetting bbox');
this.rect = this.getDefaultRect();
this.bbox = this.getDefaultRect();
@ -435,30 +294,7 @@ export class CanvasLayer {
const rect = this.konva.objectGroup.getClientRect({ skipTransform: true });
/**
* In some cases, we can use konva's getClientRect as the bbox, but there are some cases where we need to calculate
* the bbox using pixel data:
*
* - Eraser lines are normal lines, except they composite as transparency. Konva's getClientRect includes them when
* calculating the bbox.
* - Clipped portions of lines will be included in the client rect.
* - Images have transparency, so they will be included in the client rect.
*
* TODO(psyche): Using pixel data is slow. Is it possible to be clever and somehow subtract the eraser lines and
* clipped areas from the client rect?
*/
let needsPixelBbox = false;
for (const obj of this.objects.values()) {
const isEraserLine = obj instanceof CanvasEraserLine;
const isImage = obj instanceof CanvasImage;
const hasClip = obj instanceof CanvasBrushLine && obj.state.clip;
if (isEraserLine || hasClip || isImage) {
needsPixelBbox = true;
break;
}
}
if (!needsPixelBbox) {
if (!this.renderer.needsPixelBbox()) {
this.rect = deepClone(rect);
this.bbox = deepClone(rect);
this.isPendingBboxCalculation = false;
@ -508,10 +344,10 @@ export class CanvasLayer {
rect: deepClone(this.rect),
bbox: deepClone(this.bbox),
bboxNeedsUpdate: this.bboxNeedsUpdate,
isFirstRender: this.isFirstRender,
isTransforming: this.isTransforming,
isPendingBboxCalculation: this.isPendingBboxCalculation,
objects: Array.from(this.objects.values()).map((obj) => obj.repr()),
transformer: this.transformer.repr(),
renderer: this.renderer.repr(),
};
};

View File

@ -2,12 +2,13 @@ import type { Store } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { RootState } from 'app/store/store';
import type { JSONObject } from 'common/types';
import type { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine';
import type { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine';
import type { CanvasImage } from 'features/controlLayers/konva/CanvasImage';
import type { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine';
import type { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine';
import type { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage';
import { CanvasInitialImage } from 'features/controlLayers/konva/CanvasInitialImage';
import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
import { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview';
import type { CanvasRect } from 'features/controlLayers/konva/CanvasRect';
import type { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect';
import type { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer';
import {
getCompositeLayerImage,
@ -593,11 +594,12 @@ export class CanvasManager {
buildGetLoggingContext = (
instance:
| CanvasBrushLine
| CanvasEraserLine
| CanvasRect
| CanvasImage
| CanvasBrushLineRenderer
| CanvasEraserLineRenderer
| CanvasRectRenderer
| CanvasImageRenderer
| CanvasTransformer
| CanvasObjectRenderer
| CanvasLayer
| CanvasStagingArea
): GetLoggingContext => {
@ -609,6 +611,14 @@ export class CanvasManager {
...extra,
};
};
} else if (instance instanceof CanvasObjectRenderer) {
return (extra?: JSONObject): JSONObject => {
return {
...instance.parent.getLoggingContext(),
rendererId: instance.id,
...extra,
};
};
} else {
return (extra?: JSONObject): JSONObject => {
return {

View File

@ -0,0 +1,215 @@
import type { JSONObject } from 'common/types';
import { deepClone } from 'common/util/deepClone';
import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine';
import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine';
import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage';
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import type {
CanvasBrushLineState,
CanvasEraserLineState,
CanvasImageState,
CanvasRectState,
} from 'features/controlLayers/store/types';
import type { Logger } from 'roarr';
import { assert } from 'tsafe';
type AnyObjectRenderer = CanvasBrushLineRenderer | CanvasEraserLineRenderer | CanvasRectRenderer | CanvasImageRenderer;
type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImageState | CanvasRectState;
export class CanvasObjectRenderer {
static TYPE = 'object_renderer';
static OBJECT_GROUP_NAME = `${CanvasObjectRenderer.TYPE}_group`;
id: string;
parent: CanvasLayer;
manager: CanvasManager;
log: Logger;
getLoggingContext: (extra?: JSONObject) => JSONObject;
isFirstRender: boolean = true;
isRendering: boolean = false;
buffer: AnyObjectState | null = null;
renderers: Map<string, AnyObjectRenderer> = new Map();
constructor(parent: CanvasLayer) {
this.id = getPrefixedId(CanvasObjectRenderer.TYPE);
this.parent = parent;
this.manager = parent.manager;
this.getLoggingContext = this.manager.buildGetLoggingContext(this);
this.log = this.manager.buildLogger(this.getLoggingContext);
this.log.trace('Creating object renderer');
}
render = async (objectStates: AnyObjectState[]): Promise<boolean> => {
this.isRendering = true;
let didRender = false;
const objectIds = objectStates.map((objectState) => objectState.id);
for (const renderer of this.renderers.values()) {
if (!objectIds.includes(renderer.id) && renderer.id !== this.buffer?.id) {
this.renderers.delete(renderer.id);
renderer.destroy();
didRender = true;
}
}
for (const objectState of objectStates) {
didRender = (await this.renderObject(objectState)) || didRender;
}
if (this.buffer) {
didRender = (await this.renderObject(this.buffer)) || didRender;
}
this.isRendering = false;
this.isFirstRender = false;
return didRender;
};
renderObject = async (objectState: AnyObjectState, force?: boolean): Promise<boolean> => {
let didRender = false;
if (objectState.type === 'brush_line') {
let renderer = this.renderers.get(objectState.id);
assert(renderer instanceof CanvasBrushLineRenderer || renderer === undefined);
if (!renderer) {
renderer = new CanvasBrushLineRenderer(objectState, this);
this.renderers.set(renderer.id, renderer);
this.parent.konva.objectGroup.add(renderer.konva.group);
}
didRender = renderer.update(objectState, force);
} else if (objectState.type === 'eraser_line') {
let renderer = this.renderers.get(objectState.id);
assert(renderer instanceof CanvasEraserLineRenderer || renderer === undefined);
if (!renderer) {
renderer = new CanvasEraserLineRenderer(objectState, this);
this.renderers.set(renderer.id, renderer);
this.parent.konva.objectGroup.add(renderer.konva.group);
}
didRender = renderer.update(objectState, force);
} else if (objectState.type === 'rect') {
let renderer = this.renderers.get(objectState.id);
assert(renderer instanceof CanvasRectRenderer || renderer === undefined);
if (!renderer) {
renderer = new CanvasRectRenderer(objectState, this);
this.renderers.set(renderer.id, renderer);
this.parent.konva.objectGroup.add(renderer.konva.group);
}
didRender = renderer.update(objectState, force);
} else if (objectState.type === 'image') {
let renderer = this.renderers.get(objectState.id);
assert(renderer instanceof CanvasImageRenderer || renderer === undefined);
if (!renderer) {
renderer = new CanvasImageRenderer(objectState, this);
this.renderers.set(renderer.id, renderer);
this.parent.konva.objectGroup.add(renderer.konva.group);
}
didRender = await renderer.update(objectState, force);
}
this.isFirstRender = false;
return didRender;
};
hasBuffer = (): boolean => {
return this.buffer !== null;
};
setBuffer = async (objectState: AnyObjectState): Promise<boolean> => {
this.buffer = objectState;
return await this.renderObject(this.buffer, true);
};
clearBuffer = () => {
this.buffer = null;
};
commitBuffer = () => {
if (!this.buffer) {
return;
}
// We need to give the objects a fresh ID else they will be considered the same object when they are re-rendered as
// a non-buffer object, and we won't trigger things like bbox calculation
this.buffer.id = getPrefixedId(this.buffer.type);
if (this.buffer.type === 'brush_line') {
this.manager.stateApi.onBrushLineAdded({ id: this.parent.id, brushLine: this.buffer }, 'layer');
} else if (this.buffer.type === 'eraser_line') {
this.manager.stateApi.onEraserLineAdded({ id: this.parent.id, eraserLine: this.buffer }, 'layer');
} else if (this.buffer.type === 'rect') {
this.manager.stateApi.onRectShapeAdded({ id: this.parent.id, rectShape: this.buffer }, 'layer');
} else {
this.log.warn({ buffer: this.buffer }, 'Invalid buffer object type');
}
this.buffer = null;
};
/**
* Determines if the objects in the renderer require a pixel bbox calculation.
*
* In some cases, we can use Konva's getClientRect as the bbox, but it is not always accurate. It includes
* these visually transparent shapes in its calculation:
*
* - Eraser lines, which are normal lines with a globalCompositeOperation of 'destination-out'.
* - Clipped portions of any shape.
* - Images, which may have transparent areas.
*/
needsPixelBbox = (): boolean => {
let needsPixelBbox = false;
for (const renderer of this.renderers.values()) {
const isEraserLine = renderer instanceof CanvasEraserLineRenderer;
const isImage = renderer instanceof CanvasImageRenderer;
const hasClip = renderer instanceof CanvasBrushLineRenderer && renderer.state.clip;
if (isEraserLine || hasClip || isImage) {
needsPixelBbox = true;
break;
}
}
return needsPixelBbox;
};
hasObjects = (): boolean => {
return this.renderers.size > 0 || this.buffer !== null;
};
hideAll = (except: string[]) => {
for (const renderer of this.renderers.values()) {
if (!except.includes(renderer.id)) {
renderer.setVisibility(false);
}
}
};
destroy = () => {
this.log.trace('Destroying object renderer');
for (const renderer of this.renderers.values()) {
renderer.destroy();
}
this.renderers.clear();
};
repr = () => {
return {
id: this.id,
type: CanvasObjectRenderer.TYPE,
parent: this.parent.id,
renderers: Array.from(this.renderers.values()).map((renderer) => renderer.repr()),
buffer: deepClone(this.buffer),
isFirstRender: this.isFirstRender,
isRendering: this.isRendering,
};
};
}

View File

@ -1,18 +1,18 @@
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import { deepClone } from 'common/util/deepClone';
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import type { GetLoggingContext, CanvasRectState } from 'features/controlLayers/store/types';
import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
import type { CanvasRectState, GetLoggingContext } from 'features/controlLayers/store/types';
import Konva from 'konva';
import type { Logger } from 'roarr';
export class CanvasRect {
export class CanvasRectRenderer {
static TYPE = 'rect';
static GROUP_NAME = `${CanvasRect.TYPE}_group`;
static RECT_NAME = `${CanvasRect.TYPE}_rect`;
static GROUP_NAME = `${CanvasRectRenderer.TYPE}_group`;
static RECT_NAME = `${CanvasRectRenderer.TYPE}_rect`;
id: string;
parent: CanvasLayer;
parent: CanvasObjectRenderer;
manager: CanvasManager;
log: Logger;
getLoggingContext: GetLoggingContext;
@ -22,8 +22,9 @@ export class CanvasRect {
group: Konva.Group;
rect: Konva.Rect;
};
isFirstRender: boolean = false;
constructor(state: CanvasRectState, parent: CanvasLayer) {
constructor(state: CanvasRectState, parent: CanvasObjectRenderer) {
const { id, x, y, width, height, color } = state;
this.id = id;
this.parent = parent;
@ -33,9 +34,9 @@ export class CanvasRect {
this.log.trace({ state }, 'Creating rect');
this.konva = {
group: new Konva.Group({ name: CanvasRect.GROUP_NAME, listening: false }),
group: new Konva.Group({ name: CanvasRectRenderer.GROUP_NAME, listening: false }),
rect: new Konva.Rect({
name: CanvasRect.RECT_NAME,
name: CanvasRectRenderer.RECT_NAME,
x,
y,
width,
@ -48,8 +49,10 @@ export class CanvasRect {
this.state = state;
}
update(state: CanvasRectState, force?: boolean): boolean {
update(state: CanvasRectState, force = this.isFirstRender): boolean {
if (this.state !== state || force) {
this.isFirstRender = false;
this.log.trace({ state }, 'Updating rect');
const { x, y, width, height, color } = state;
this.konva.rect.setAttrs({
@ -61,9 +64,9 @@ export class CanvasRect {
});
this.state = state;
return true;
} else {
return false;
}
return false;
}
destroy() {
@ -79,8 +82,9 @@ export class CanvasRect {
repr() {
return {
id: this.id,
type: CanvasRect.TYPE,
type: CanvasRectRenderer.TYPE,
parent: this.parent.id,
isFirstRender: this.isFirstRender,
state: deepClone(this.state),
};
}

View File

@ -1,8 +1,8 @@
import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine';
import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine';
import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine';
import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasRect } from 'features/controlLayers/konva/CanvasRect';
import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect';
import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox';
import { mapId } from 'features/controlLayers/konva/util';
import type { CanvasBrushLineState, CanvasEraserLineState, CanvasRectState, CanvasRegionalGuidanceState } from 'features/controlLayers/store/types';
@ -32,7 +32,7 @@ export class CanvasRegion {
transformer: Konva.Transformer;
};
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect>;
objects: Map<string, CanvasBrushLineRenderer | CanvasEraserLineRenderer | CanvasRectRenderer>;
constructor(state: CanvasRegionalGuidanceState, manager: CanvasManager) {
this.id = state.id;
@ -155,10 +155,10 @@ export class CanvasRegion {
private async renderObject(obj: CanvasRegionalGuidanceState['objects'][number], force = false): Promise<boolean> {
if (obj.type === 'brush_line') {
let brushLine = this.objects.get(obj.id);
assert(brushLine instanceof CanvasBrushLine || brushLine === undefined);
assert(brushLine instanceof CanvasBrushLineRenderer || brushLine === undefined);
if (!brushLine) {
brushLine = new CanvasBrushLine(obj);
brushLine = new CanvasBrushLineRenderer(obj);
this.objects.set(brushLine.id, brushLine);
this.konva.objectGroup.add(brushLine.konva.group);
return true;
@ -169,10 +169,10 @@ export class CanvasRegion {
}
} else if (obj.type === 'eraser_line') {
let eraserLine = this.objects.get(obj.id);
assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined);
assert(eraserLine instanceof CanvasEraserLineRenderer || eraserLine === undefined);
if (!eraserLine) {
eraserLine = new CanvasEraserLine(obj);
eraserLine = new CanvasEraserLineRenderer(obj);
this.objects.set(eraserLine.id, eraserLine);
this.konva.objectGroup.add(eraserLine.konva.group);
return true;
@ -183,10 +183,10 @@ export class CanvasRegion {
}
} else if (obj.type === 'rect') {
let rect = this.objects.get(obj.id);
assert(rect instanceof CanvasRect || rect === undefined);
assert(rect instanceof CanvasRectRenderer || rect === undefined);
if (!rect) {
rect = new CanvasRect(obj);
rect = new CanvasRectRenderer(obj);
this.objects.set(rect.id, rect);
this.konva.objectGroup.add(rect.konva.group);
return true;

View File

@ -1,4 +1,4 @@
import { CanvasImage } from 'features/controlLayers/konva/CanvasImage';
import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import type { GetLoggingContext, StagingAreaImage } from 'features/controlLayers/store/types';
@ -16,7 +16,7 @@ export class CanvasStagingArea {
konva: { group: Konva.Group };
image: CanvasImage | null;
image: CanvasImageRenderer | null;
selectedImage: StagingAreaImage | null;
constructor(manager: CanvasManager) {
@ -43,7 +43,7 @@ export class CanvasStagingArea {
if (!this.image) {
const { image_name, width, height } = imageDTO;
this.image = new CanvasImage(
this.image = new CanvasImageRenderer(
{
id: 'staging-area-image',
type: 'image',

View File

@ -46,22 +46,16 @@ export class CanvasTransformer {
*/
isTransformEnabled: boolean;
/**
* The konva group that the transformer will manipulate.
*/
transformTarget: Konva.Group;
konva: {
transformer: Konva.Transformer;
proxyRect: Konva.Rect;
bboxOutline: Konva.Rect;
};
constructor(parent: CanvasLayer, transformTarget: Konva.Group) {
constructor(parent: CanvasLayer) {
this.id = getPrefixedId(CanvasTransformer.TYPE);
this.parent = parent;
this.manager = parent.manager;
this.transformTarget = transformTarget;
this.getLoggingContext = this.manager.buildGetLoggingContext(this);
this.log = this.manager.buildLogger(this.getLoggingContext);
@ -192,7 +186,7 @@ export class CanvasTransformer {
// This is called when a transform anchor is dragged. By this time, the transform constraints in the above
// callbacks have been enforced, and the transformer has updated its nodes' attributes. We need to pass the
// updated attributes to the object group, propagating the transformation on down.
this.transformTarget.setAttrs({
this.parent.konva.objectGroup.setAttrs({
x: this.konva.proxyRect.x(),
y: this.konva.proxyRect.y(),
scaleX: this.konva.proxyRect.scaleX(),
@ -234,7 +228,7 @@ export class CanvasTransformer {
scaleX: snappedScaleX,
scaleY: snappedScaleY,
});
this.transformTarget.setAttrs({
this.parent.konva.objectGroup.setAttrs({
x: snappedX,
y: snappedY,
scaleX: snappedScaleX,
@ -278,7 +272,7 @@ export class CanvasTransformer {
// The object group is translated by the difference between the interaction rect's new and old positions (which is
// stored as this.bbox)
this.transformTarget.setAttrs({
this.parent.konva.objectGroup.setAttrs({
x: this.konva.proxyRect.x(),
y: this.konva.proxyRect.y(),
});

View File

@ -6,11 +6,11 @@ import {
offsetCoord,
} from 'features/controlLayers/konva/util';
import type {
CanvasV2State,
Coordinate,
CanvasInpaintMaskState,
CanvasLayerState,
CanvasRegionalGuidanceState,
CanvasV2State,
Coordinate,
Tool,
} from 'features/controlLayers/store/types';
import { isDrawableEntity, isDrawableEntityAdapter } from 'features/controlLayers/store/types';
@ -189,11 +189,11 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
if (e.evt.shiftKey && lastLinePoint) {
// Create a straight line from the last line point
if (selectedEntityAdapter.getDrawingBuffer()) {
await selectedEntityAdapter.finalizeDrawingBuffer();
if (selectedEntityAdapter.renderer.buffer) {
await selectedEntityAdapter.renderer.commitBuffer();
}
await selectedEntityAdapter.setDrawingBuffer({
await selectedEntityAdapter.renderer.setBuffer({
id: getObjectId('brush_line', true),
type: 'brush_line',
points: [
@ -208,10 +208,10 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
clip: getClip(selectedEntity),
});
} else {
if (selectedEntityAdapter.getDrawingBuffer()) {
await selectedEntityAdapter.finalizeDrawingBuffer();
if (selectedEntityAdapter.renderer.buffer) {
await selectedEntityAdapter.renderer.commitBuffer();
}
await selectedEntityAdapter.setDrawingBuffer({
await selectedEntityAdapter.renderer.setBuffer({
id: getObjectId('brush_line', true),
type: 'brush_line',
points: [alignedPoint.x, alignedPoint.y],
@ -228,10 +228,10 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
if (e.evt.shiftKey && lastLinePoint) {
// Create a straight line from the last line point
if (selectedEntityAdapter.getDrawingBuffer()) {
await selectedEntityAdapter.finalizeDrawingBuffer();
if (selectedEntityAdapter.renderer.buffer) {
await selectedEntityAdapter.renderer.commitBuffer();
}
await selectedEntityAdapter.setDrawingBuffer({
await selectedEntityAdapter.renderer.setBuffer({
id: getObjectId('eraser_line', true),
type: 'eraser_line',
points: [
@ -245,10 +245,10 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
clip: getClip(selectedEntity),
});
} else {
if (selectedEntityAdapter.getDrawingBuffer()) {
await selectedEntityAdapter.finalizeDrawingBuffer();
if (selectedEntityAdapter.renderer.buffer) {
await selectedEntityAdapter.renderer.commitBuffer();
}
await selectedEntityAdapter.setDrawingBuffer({
await selectedEntityAdapter.renderer.setBuffer({
id: getObjectId('eraser_line', true),
type: 'eraser_line',
points: [alignedPoint.x, alignedPoint.y],
@ -260,10 +260,10 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
}
if (toolState.selected === 'rect') {
if (selectedEntityAdapter.getDrawingBuffer()) {
await selectedEntityAdapter.finalizeDrawingBuffer();
if (selectedEntityAdapter.renderer.buffer) {
await selectedEntityAdapter.renderer.commitBuffer();
}
await selectedEntityAdapter.setDrawingBuffer({
await selectedEntityAdapter.renderer.setBuffer({
id: getObjectId('rect', true),
type: 'rect',
x: Math.round(normalizedPoint.x),
@ -295,29 +295,29 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
const toolState = getToolState();
if (toolState.selected === 'brush') {
const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
if (drawingBuffer?.type === 'brush_line') {
await selectedEntityAdapter.finalizeDrawingBuffer();
await selectedEntityAdapter.renderer.commitBuffer();
} else {
await selectedEntityAdapter.setDrawingBuffer(null);
await selectedEntityAdapter.renderer.clearBuffer();
}
}
if (toolState.selected === 'eraser') {
const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
if (drawingBuffer?.type === 'eraser_line') {
await selectedEntityAdapter.finalizeDrawingBuffer();
await selectedEntityAdapter.renderer.commitBuffer();
} else {
await selectedEntityAdapter.setDrawingBuffer(null);
await selectedEntityAdapter.renderer.clearBuffer();
}
}
if (toolState.selected === 'rect') {
const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
if (drawingBuffer?.type === 'rect') {
await selectedEntityAdapter.finalizeDrawingBuffer();
await selectedEntityAdapter.renderer.commitBuffer();
} else {
await selectedEntityAdapter.setDrawingBuffer(null);
await selectedEntityAdapter.renderer.clearBuffer();
}
}
@ -344,7 +344,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
getIsPrimaryMouseDown(e)
) {
if (toolState.selected === 'brush') {
const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
if (drawingBuffer) {
if (drawingBuffer?.type === 'brush_line') {
const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points));
@ -352,19 +352,19 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
const normalizedPoint = offsetCoord(nextPoint, selectedEntity.position);
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
await selectedEntityAdapter.renderer.setBuffer(drawingBuffer);
setLastAddedPoint(alignedPoint);
}
} else {
await selectedEntityAdapter.setDrawingBuffer(null);
await selectedEntityAdapter.renderer.clearBuffer();
}
} else {
if (selectedEntityAdapter.getDrawingBuffer()) {
await selectedEntityAdapter.finalizeDrawingBuffer();
if (selectedEntityAdapter.renderer.buffer) {
await selectedEntityAdapter.renderer.commitBuffer();
}
const normalizedPoint = offsetCoord(pos, selectedEntity.position);
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
await selectedEntityAdapter.setDrawingBuffer({
await selectedEntityAdapter.renderer.setBuffer({
id: getObjectId('brush_line', true),
type: 'brush_line',
points: [alignedPoint.x, alignedPoint.y],
@ -377,7 +377,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
}
if (toolState.selected === 'eraser') {
const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
if (drawingBuffer) {
if (drawingBuffer.type === 'eraser_line') {
const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points));
@ -385,19 +385,19 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
const normalizedPoint = offsetCoord(nextPoint, selectedEntity.position);
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
await selectedEntityAdapter.renderer.setBuffer(drawingBuffer);
setLastAddedPoint(alignedPoint);
}
} else {
await selectedEntityAdapter.setDrawingBuffer(null);
await selectedEntityAdapter.renderer.clearBuffer();
}
} else {
if (selectedEntityAdapter.getDrawingBuffer()) {
await selectedEntityAdapter.finalizeDrawingBuffer();
if (selectedEntityAdapter.renderer.buffer) {
await selectedEntityAdapter.renderer.commitBuffer();
}
const normalizedPoint = offsetCoord(pos, selectedEntity.position);
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
await selectedEntityAdapter.setDrawingBuffer({
await selectedEntityAdapter.renderer.setBuffer({
id: getObjectId('eraser_line', true),
type: 'eraser_line',
points: [alignedPoint.x, alignedPoint.y],
@ -409,15 +409,15 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
}
if (toolState.selected === 'rect') {
const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
if (drawingBuffer) {
if (drawingBuffer.type === 'rect') {
const normalizedPoint = offsetCoord(pos, selectedEntity.position);
drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x);
drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y);
await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
await selectedEntityAdapter.renderer.setBuffer(drawingBuffer);
} else {
await selectedEntityAdapter.setDrawingBuffer(null);
await selectedEntityAdapter.renderer.clearBuffer();
}
}
}
@ -443,23 +443,23 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
!getSpaceKey() &&
getIsPrimaryMouseDown(e)
) {
const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
const normalizedPoint = offsetCoord(pos, selectedEntity.position);
if (toolState.selected === 'brush' && drawingBuffer?.type === 'brush_line') {
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
await selectedEntityAdapter.finalizeDrawingBuffer();
await selectedEntityAdapter.renderer.setBuffer(drawingBuffer);
await selectedEntityAdapter.renderer.commitBuffer();
} else if (toolState.selected === 'eraser' && drawingBuffer?.type === 'eraser_line') {
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
await selectedEntityAdapter.finalizeDrawingBuffer();
await selectedEntityAdapter.renderer.setBuffer(drawingBuffer);
await selectedEntityAdapter.renderer.commitBuffer();
} else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect') {
drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x);
drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y);
await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
await selectedEntityAdapter.finalizeDrawingBuffer();
await selectedEntityAdapter.renderer.setBuffer(drawingBuffer);
await selectedEntityAdapter.renderer.commitBuffer();
}
}

View File

@ -1,7 +1,6 @@
import { getImageDataTransparency } from 'common/util/arrayBuffer';
import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import type { Coordinate, GenerationMode, Rect, CanvasObjectState, RgbaColor } from 'features/controlLayers/store/types';
import type { CanvasObjectState, Coordinate, GenerationMode, Rect, RgbaColor } from 'features/controlLayers/store/types';
import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers';
import Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node';
@ -414,8 +413,6 @@ export function getCompositeLayerStageClone(arg: { manager: CanvasManager }): Ko
if (!layer) {
console.log('deleting', konvaLayer);
toDelete.push(konvaLayer);
} else {
konvaLayer.findOne<Konva.Group>(`.${CanvasLayer.GROUP_NAME}`)?.findOne(`.${CanvasLayer.BBOX_NAME}`)?.destroy();
}
}

View File

@ -572,8 +572,16 @@ const zCanvasImageState = z.object({
});
export type CanvasImageState = z.infer<typeof zCanvasImageState>;
const zCanvasObjectState = z.discriminatedUnion('type', [zCanvasImageState, zCanvasBrushLineState, zCanvasEraserLineState, zCanvasRectState]);
const zCanvasObjectState = z.discriminatedUnion('type', [
zCanvasImageState,
zCanvasBrushLineState,
zCanvasEraserLineState,
zCanvasRectState,
]);
export type CanvasObjectState = z.infer<typeof zCanvasObjectState>;
export function isCanvasBrushLineState(obj: CanvasObjectState): obj is CanvasBrushLineState {
return obj.type === 'brush_line';
}
export const zCanvasLayerState = z.object({
id: zId,
@ -603,7 +611,13 @@ export type IPAdapterConfig = Pick<
>;
const zMaskObject = z
.discriminatedUnion('type', [zOLD_VectorMaskLine, zOLD_VectorMaskRect, zCanvasBrushLineState, zCanvasEraserLineState, zCanvasRectState])
.discriminatedUnion('type', [
zOLD_VectorMaskLine,
zOLD_VectorMaskRect,
zCanvasBrushLineState,
zCanvasEraserLineState,
zCanvasRectState,
])
.transform((val) => {
// Migrate old vector mask objects to new format
if (val.type === 'vector_mask_line') {
@ -713,7 +727,10 @@ const zCanvasT2IAdapteState = zCanvasControlAdapterStateBase.extend({
});
export type CanvasT2IAdapterState = z.infer<typeof zCanvasT2IAdapteState>;
export const zCanvasControlAdapterState = z.discriminatedUnion('adapterType', [zCanvasControlNetState, zCanvasT2IAdapteState]);
export const zCanvasControlAdapterState = z.discriminatedUnion('adapterType', [
zCanvasControlNetState,
zCanvasT2IAdapteState,
]);
export type CanvasControlAdapterState = z.infer<typeof zCanvasControlAdapterState>;
export type ControlNetConfig = Pick<
CanvasControlNetState,
@ -949,7 +966,9 @@ export type RemoveIndexString<T> = {
export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint';
export function isDrawableEntity(entity: CanvasEntity): entity is CanvasLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState {
export function isDrawableEntity(
entity: CanvasEntity
): entity is CanvasLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState {
return entity.type === 'layer' || entity.type === 'regional_guidance' || entity.type === 'inpaint_mask';
}