feat(ui): revised logging and naming setup, fix staging area

This commit is contained in:
psychedelicious 2024-07-31 20:04:14 +10:00
parent 3a9f955388
commit d9487c1df4
20 changed files with 309 additions and 247 deletions

View File

@ -2,10 +2,12 @@ import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import {
$lastProgressEvent,
layerAddedFromStagingArea,
layerAdded,
sessionStagingAreaImageAccepted,
sessionStagingAreaReset,
} from 'features/controlLayers/store/canvasV2Slice';
import type { LayerEntity } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { queueApi } from 'services/api/endpoints/queue';
@ -50,7 +52,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: sessionStagingAreaImageAccepted,
effect: async (action, api) => {
effect: (action, api) => {
const { index } = action.payload;
const state = api.getState();
const stagingAreaImage = state.canvasV2.session.stagedImages[index];
@ -58,7 +60,14 @@ export const addStagingListeners = (startAppListening: AppStartListening) => {
assert(stagingAreaImage, 'No staged image found to accept');
const { x, y } = state.canvasV2.bbox.rect;
api.dispatch(layerAddedFromStagingArea({ stagingAreaImage, position: { x, y } }));
const { imageDTO, offsetX, offsetY } = stagingAreaImage;
const imageObject = imageDTOToImageObject(imageDTO);
const overrides: Partial<LayerEntity> = {
position: { x: x + offsetX, y: y + offsetY },
objects: [imageObject],
};
api.dispatch(layerAdded({ overrides }));
api.dispatch(sessionStagingAreaReset());
},
});

View File

@ -1,32 +1,27 @@
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import { deepClone } from 'common/util/deepClone';
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
import { CanvasObject } from 'features/controlLayers/konva/CanvasObject';
import type { BrushLine } from 'features/controlLayers/store/types';
import Konva from 'konva';
export class CanvasBrushLine {
export class CanvasBrushLine extends CanvasObject {
static NAME_PREFIX = 'brush-line';
static GROUP_NAME = `${CanvasBrushLine.NAME_PREFIX}_group`;
static LINE_NAME = `${CanvasBrushLine.NAME_PREFIX}_line`;
static TYPE = 'brush_line';
state: BrushLine;
type = 'brush_line';
id: string;
konva: {
group: Konva.Group;
line: Konva.Line;
};
parent: CanvasLayer;
constructor(state: BrushLine, parent: CanvasLayer) {
const { id, strokeWidth, clip, color, points } = state;
super(state.id, parent);
this._log.trace({ state }, 'Creating brush line');
this.id = id;
this.parent = parent;
this.parent._log.trace(`Creating brush line ${this.id}`);
const { strokeWidth, clip, color, points } = state;
this.konva = {
group: new Konva.Group({
@ -36,7 +31,6 @@ export class CanvasBrushLine {
}),
line: new Konva.Line({
name: CanvasBrushLine.LINE_NAME,
id,
listening: false,
shadowForStrokeEnabled: false,
strokeWidth,
@ -55,7 +49,7 @@ export class CanvasBrushLine {
update(state: BrushLine, force?: boolean): boolean {
if (force || this.state !== state) {
this.parent._log.trace(`Updating brush line ${this.id}`);
this._log.trace({ state }, 'Updating brush line');
const { points, color, clip, strokeWidth } = state;
this.konva.line.setAttrs({
// A line with only one point will not be rendered, so we duplicate the points to make it visible
@ -72,23 +66,20 @@ export class CanvasBrushLine {
}
destroy() {
this.parent._log.trace(`Destroying brush line ${this.id}`);
this._log.trace('Destroying brush line');
this.konva.group.destroy();
}
show() {
this.konva.group.visible(true);
}
hide() {
this.konva.group.visible(false);
setVisibility(isVisible: boolean): void {
this._log.trace({ isVisible }, 'Setting brush line visibility');
this.konva.group.visible(isVisible);
}
repr() {
return {
id: this.id,
type: this.type,
parent: this.parent.id,
type: CanvasBrushLine.TYPE,
parent: this._parent.id,
state: deepClone(this.state),
};
}

View File

@ -0,0 +1,27 @@
import type { JSONObject } from 'common/types';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import type { Logger } from 'roarr';
export abstract class CanvasEntity {
id: string;
_manager: CanvasManager;
_log: Logger;
constructor(id: string, manager: CanvasManager) {
this.id = id;
this._manager = manager;
this._log = this._manager.buildLogger(this._getLoggingContext);
}
/**
* Get a serializable representation of the entity.
*/
abstract repr(): JSONObject;
_getLoggingContext = (extra?: Record<string, unknown>) => {
return {
...this._manager._getLoggingContext(),
layerId: this.id,
...extra,
};
};
}

View File

@ -1,33 +1,28 @@
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import { deepClone } from 'common/util/deepClone';
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
import { CanvasObject } from 'features/controlLayers/konva/CanvasObject';
import type { EraserLine } from 'features/controlLayers/store/types';
import { RGBA_RED } from 'features/controlLayers/store/types';
import Konva from 'konva';
export class CanvasEraserLine {
export class CanvasEraserLine extends CanvasObject {
static NAME_PREFIX = 'eraser-line';
static GROUP_NAME = `${CanvasEraserLine.NAME_PREFIX}_group`;
static LINE_NAME = `${CanvasEraserLine.NAME_PREFIX}_line`;
static TYPE = 'eraser_line';
state: EraserLine;
type = 'eraser_line';
id: string;
konva: {
group: Konva.Group;
line: Konva.Line;
};
parent: CanvasLayer;
constructor(state: EraserLine, parent: CanvasLayer) {
const { id, strokeWidth, clip, points } = state;
super(state.id, parent);
this._log.trace({ state }, 'Creating eraser line');
this.id = id;
this.parent = parent;
this.parent._log.trace(`Creating eraser line ${this.id}`);
const { strokeWidth, clip, points } = state;
this.konva = {
group: new Konva.Group({
@ -37,7 +32,6 @@ export class CanvasEraserLine {
}),
line: new Konva.Line({
name: CanvasEraserLine.LINE_NAME,
id,
listening: false,
shadowForStrokeEnabled: false,
strokeWidth,
@ -56,7 +50,7 @@ export class CanvasEraserLine {
update(state: EraserLine, force?: boolean): boolean {
if (force || this.state !== state) {
this.parent._log.trace(`Updating eraser line ${this.id}`);
this._log.trace({ state }, 'Updating eraser line');
const { points, clip, strokeWidth } = state;
this.konva.line.setAttrs({
// A line with only one point will not be rendered, so we duplicate the points to make it visible
@ -72,23 +66,20 @@ export class CanvasEraserLine {
}
destroy() {
this.parent._log.trace(`Destroying eraser line ${this.id}`);
this._log.trace('Destroying eraser line');
this.konva.group.destroy();
}
show() {
this.konva.group.visible(true);
}
hide() {
this.konva.group.visible(false);
setVisibility(isVisible: boolean): void {
this._log.trace({ isVisible }, 'Setting brush line visibility');
this.konva.group.visible(isVisible);
}
repr() {
return {
id: this.id,
type: this.type,
parent: this.parent.id,
type: CanvasEraserLine.TYPE,
parent: this._parent.id,
state: deepClone(this.state),
};
}

View File

@ -1,26 +1,24 @@
import { deepClone } from 'common/util/deepClone';
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
import { CanvasObject } from 'features/controlLayers/konva/CanvasObject';
import type { CanvasStagingArea } from 'features/controlLayers/konva/CanvasStagingArea';
import { FILTER_MAP } from 'features/controlLayers/konva/filters';
import { loadImage } from 'features/controlLayers/konva/util';
import type { ImageObject } from 'features/controlLayers/store/types';
import { t } from 'i18next';
import Konva from 'konva';
import { getImageDTO } from 'services/api/endpoints/images';
import { assert } from 'tsafe';
export class CanvasImage {
export class CanvasImage extends CanvasObject {
static NAME_PREFIX = 'canvas-image';
static GROUP_NAME = `${CanvasImage.NAME_PREFIX}_group`;
static IMAGE_NAME = `${CanvasImage.NAME_PREFIX}_image`;
static PLACEHOLDER_GROUP_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-group`;
static PLACEHOLDER_RECT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-rect`;
static PLACEHOLDER_TEXT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-text`;
static TYPE = 'image';
state: ImageObject;
type = 'image';
id: string;
konva: {
group: Konva.Group;
placeholder: { group: Konva.Group; rect: Konva.Rect; text: Konva.Text };
@ -30,14 +28,11 @@ export class CanvasImage {
isLoading: boolean;
isError: boolean;
parent: CanvasLayer;
constructor(state: ImageObject, parent: CanvasLayer | CanvasStagingArea) {
super(state.id, parent);
this._log.trace({ state }, 'Creating image');
constructor(state: ImageObject, parent: CanvasLayer) {
const { id, width, height, x, y } = state;
this.id = id;
this.parent = parent;
this.parent._log.trace(`Creating image ${this.id}`);
const { width, height, x, y } = state;
this.konva = {
group: new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }),
@ -78,7 +73,7 @@ export class CanvasImage {
async updateImageSource(imageName: string) {
try {
this.parent._log.trace(`Updating image source ${this.id}`);
this._log.trace({ imageName }, 'Updating image source');
this.isLoading = true;
this.konva.group.visible(true);
@ -89,7 +84,10 @@ export class CanvasImage {
}
const imageDTO = await getImageDTO(imageName);
assert(imageDTO !== null, 'imageDTO is null');
if (imageDTO === null) {
this._log.error({ imageName }, 'Image not found');
return;
}
const imageEl = await loadImage(imageDTO.image_url);
if (this.konva.image) {
@ -120,6 +118,7 @@ export class CanvasImage {
this.isError = false;
this.konva.placeholder.group.visible(false);
} catch {
this._log({ imageName }, 'Failed to load image');
this.konva.image?.visible(false);
this.imageName = null;
this.isLoading = false;
@ -131,7 +130,7 @@ export class CanvasImage {
async update(state: ImageObject, force?: boolean): Promise<boolean> {
if (this.state !== state || force) {
this.parent._log.trace(`Updating image ${this.id}`);
this._log.trace({ state }, 'Updating image');
const { width, height, x, y, image, filters } = state;
if (this.state.image.name !== image.name || force) {
@ -155,23 +154,20 @@ export class CanvasImage {
}
destroy() {
this.parent._log.trace(`Destroying image ${this.id}`);
this._log.trace('Destroying image');
this.konva.group.destroy();
}
show() {
this.konva.group.visible(true);
}
hide() {
this.konva.group.visible(false);
setVisibility(isVisible: boolean): void {
this._log.trace({ isVisible }, 'Setting image visibility');
this.konva.group.visible(isVisible);
}
repr() {
return {
id: this.id,
type: this.type,
parent: this.parent.id,
type: CanvasImage.TYPE,
parent: this._parent.id,
imageName: this.imageName,
isLoading: this.isLoading,
isError: this.isError,

View File

@ -1,12 +1,12 @@
import { getStore } from 'app/store/nanostores/store';
import { deepClone } from 'common/util/deepClone';
import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine';
import { CanvasEntity } from 'features/controlLayers/konva/CanvasEntity';
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 { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming';
import { konvaNodeToBlob, mapId, nanoid, previewBlob } from 'features/controlLayers/konva/util';
import { getPrefixedId, konvaNodeToBlob, mapId, previewBlob } from 'features/controlLayers/konva/util';
import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice';
import {
type BrushLine,
@ -20,11 +20,10 @@ import {
} from 'features/controlLayers/store/types';
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 {
export class CanvasLayer extends CanvasEntity {
static NAME_PREFIX = 'layer';
static LAYER_NAME = `${CanvasLayer.NAME_PREFIX}_layer`;
static TRANSFORMER_NAME = `${CanvasLayer.NAME_PREFIX}_transformer`;
@ -36,8 +35,7 @@ export class CanvasLayer {
_drawingBuffer: BrushLine | EraserLine | RectShape | null;
_state: LayerEntity;
id: string;
manager: CanvasManager;
type = 'layer';
konva: {
layer: Konva.Layer;
@ -48,7 +46,6 @@ export class CanvasLayer {
};
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage>;
_log: Logger;
_bboxNeedsUpdate: boolean;
_isFirstRender: boolean;
@ -59,8 +56,9 @@ export class CanvasLayer {
bbox: Rect;
constructor(state: LayerEntity, manager: CanvasManager) {
this.id = state.id;
this.manager = manager;
super(state.id, manager);
this._log.debug({ state }, 'Creating layer');
this.konva = {
layer: new Konva.Layer({ id: this.id, name: CanvasLayer.LAYER_NAME, listening: false }),
bbox: new Konva.Rect({
@ -79,7 +77,7 @@ export class CanvasLayer {
rotateEnabled: true,
flipEnabled: true,
listening: false,
padding: this.manager.getTransformerPadding(),
padding: this._manager.getTransformerPadding(),
stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400
keepRatio: false,
}),
@ -149,8 +147,8 @@ export class CanvasLayer {
// The bbox should be updated to reflect the new position of the interaction rect, taking into account its padding
// and border
this.konva.bbox.setAttrs({
x: this.konva.interactionRect.x() - this.manager.getScaledBboxPadding(),
y: this.konva.interactionRect.y() - this.manager.getScaledBboxPadding(),
x: this.konva.interactionRect.x() - this._manager.getScaledBboxPadding(),
y: this.konva.interactionRect.y() - this._manager.getScaledBboxPadding(),
});
// The object group is translated by the difference between the interaction rect's new and old positions (which is
@ -169,7 +167,7 @@ export class CanvasLayer {
return;
}
this.manager.stateApi.onPosChanged(
this._manager.stateApi.onPosChanged(
{
id: this.id,
position: {
@ -190,11 +188,10 @@ export class CanvasLayer {
this.isTransforming = false;
this._isFirstRender = true;
this.isPendingBboxCalculation = false;
this._log = this.manager.getLogger(`layer_${this.id}`);
}
destroy(): void {
this._log.debug(`Layer ${this.id} - destroying`);
this._log.debug('Destroying layer');
this.konva.layer.destroy();
}
@ -221,21 +218,21 @@ export class CanvasLayer {
// a non-buffer object, and we won't trigger things like bbox calculation
if (drawingBuffer.type === 'brush_line') {
drawingBuffer.id = getBrushLineId(this.id, nanoid());
this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: drawingBuffer }, 'layer');
drawingBuffer.id = getPrefixedId('brush_line');
this._manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: drawingBuffer }, 'layer');
} else if (drawingBuffer.type === 'eraser_line') {
drawingBuffer.id = getEraserLineId(this.id, nanoid());
this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: drawingBuffer }, 'layer');
drawingBuffer.id = getPrefixedId('brush_line');
this._manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: drawingBuffer }, 'layer');
} else if (drawingBuffer.type === 'rect_shape') {
drawingBuffer.id = getRectShapeId(this.id, nanoid());
this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: drawingBuffer }, 'layer');
drawingBuffer.id = getPrefixedId('brush_line');
this._manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: drawingBuffer }, 'layer');
}
}
async update(arg?: { state: LayerEntity; toolState: CanvasV2State['tool']; isSelected: boolean }) {
const state = get(arg, 'state', this._state);
const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState());
const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id));
const toolState = get(arg, 'toolState', this._manager.stateApi.getToolState());
const isSelected = get(arg, 'isSelected', this._manager.stateApi.getIsSelected(this.id));
if (!this._isFirstRender && state === this._state) {
this._log.trace('State unchanged, skipping update');
@ -277,7 +274,7 @@ export class CanvasLayer {
updatePosition(arg?: { position: Coordinate }) {
this._log.trace('Updating position');
const position = get(arg, 'position', this._state.position);
const bboxPadding = this.manager.getScaledBboxPadding();
const bboxPadding = this._manager.getScaledBboxPadding();
this.konva.objectGroup.setAttrs({
x: position.x + this.bbox.x,
@ -339,8 +336,8 @@ export class CanvasLayer {
updateInteraction(arg?: { toolState: CanvasV2State['tool']; isSelected: boolean }) {
this._log.trace('Updating interaction');
const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState());
const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id));
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) {
// The layer is totally empty, we can just disable the layer
@ -397,7 +394,7 @@ export class CanvasLayer {
if (this.bbox.width === 0 || this.bbox.height === 0) {
if (this.objects.size > 0) {
// The layer is fully transparent but has objects - reset it
this.manager.stateApi.onEntityReset({ id: this.id }, 'layer');
this._manager.stateApi.onEntityReset({ id: this.id }, 'layer');
}
this.konva.bbox.visible(false);
this.konva.interactionRect.visible(false);
@ -407,8 +404,8 @@ export class CanvasLayer {
this.konva.bbox.visible(true);
this.konva.interactionRect.visible(true);
const onePixel = this.manager.getScaledPixel();
const bboxPadding = this.manager.getScaledBboxPadding();
const onePixel = this._manager.getScaledPixel();
const bboxPadding = this._manager.getScaledBboxPadding();
this.konva.bbox.setAttrs({
x: this._state.position.x + this.bbox.x - bboxPadding,
@ -434,8 +431,8 @@ export class CanvasLayer {
syncStageScale() {
this._log.trace('Syncing scale to stage');
const onePixel = this.manager.getScaledPixel();
const bboxPadding = this.manager.getScaledBboxPadding();
const onePixel = this._manager.getScaledPixel();
const bboxPadding = this._manager.getScaledBboxPadding();
this.konva.bbox.setAttrs({
x: this.konva.interactionRect.x() - bboxPadding,
@ -515,7 +512,7 @@ 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';
const listening = this._manager.stateApi.getToolState().selected !== 'view';
this.konva.layer.listening(listening);
this.konva.interactionRect.listening(listening);
@ -546,12 +543,12 @@ export class CanvasLayer {
const interactionRectClone = this.konva.interactionRect.clone();
const rect = interactionRectClone.getClientRect();
const blob = await konvaNodeToBlob(objectGroupClone, rect);
if (this.manager._isDebugging) {
if (this._manager._isDebugging) {
previewBlob(blob, 'Rasterized layer');
}
const imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true);
const { dispatch } = getStore();
const imageObject = imageDTOToImageObject(this.id, nanoid(), imageDTO);
const imageObject = imageDTOToImageObject(imageDTO);
await this._renderObject(imageObject, true);
for (const obj of this.objects.values()) {
if (obj.id !== imageObject.id) {
@ -632,7 +629,7 @@ export class CanvasLayer {
return;
}
const imageData = ctx.getImageData(0, 0, rect.width, rect.height);
this.manager.requestBbox(
this._manager.requestBbox(
{ buffer: imageData.data.buffer, width: imageData.width, height: imageData.height },
(extents) => {
this.rect = deepClone(rect);
@ -658,7 +655,7 @@ export class CanvasLayer {
repr() {
return {
id: this.id,
type: 'layer',
type: this.type,
state: deepClone(this._state),
rect: deepClone(this.rect),
bbox: deepClone(this.bbox),

View File

@ -1,6 +1,7 @@
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 { CanvasInitialImage } from 'features/controlLayers/konva/CanvasInitialImage';
import { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview';
import {
@ -107,7 +108,15 @@ export class CanvasManager {
this._prevState = this.stateApi.getState();
this._isFirstRender = true;
this.log = logger('canvas');
this.log = logger('canvas').child((message) => {
return {
...message,
context: {
...message.context,
...this._getLoggingContext(),
},
};
});
this.workerLog = logger('worker');
this.util = {
@ -173,11 +182,6 @@ export class CanvasManager {
this._isDebugging = false;
}
getLogger(namespace: string) {
const managerNamespace = this.log.getContext().namespace;
return this.log.child({ namespace: `${managerNamespace}.${namespace}` });
}
requestBbox(data: Omit<GetBboxTask['data'], 'id'>, onComplete: (extents: Extents | null) => void) {
const id = nanoid();
const task: GetBboxTask = {
@ -330,7 +334,6 @@ export class CanvasManager {
for (const canvasLayer of this.layers.values()) {
if (!state.layers.entities.find((l) => l.id === canvasLayer.id)) {
this.log.debug(`Destroying deleted layer ${canvasLayer.id}`);
await canvasLayer.destroy();
this.layers.delete(canvasLayer.id);
}
@ -339,7 +342,6 @@ export class CanvasManager {
for (const entityState of state.layers.entities) {
let adapter = this.layers.get(entityState.id);
if (!adapter) {
this.log.debug(`Creating layer layer ${entityState.id}`);
adapter = new CanvasLayer(entityState, this);
this.layers.set(adapter.id, adapter);
this.stage.add(adapter.konva.layer);
@ -562,9 +564,29 @@ export class CanvasManager {
}
}
_getLoggingContext() {
return {
// timestamp: new Date().toISOString(),
};
}
buildLogger(getContext: () => JSONObject): Logger {
return this.log.child((message) => {
return {
...message,
context: {
...message.context,
...getContext(),
},
};
});
}
logDebugInfo() {
// eslint-disable-next-line no-console
console.log(this);
for (const layer of this.layers.values()) {
// eslint-disable-next-line no-console
console.log(layer);
}
}

View File

@ -0,0 +1,48 @@
import type { JSONObject } from 'common/types';
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 { Logger } from 'roarr';
export abstract class CanvasObject {
id: string;
_parent: CanvasLayer | CanvasStagingArea;
_manager: CanvasManager;
_log: Logger;
constructor(id: string, parent: CanvasLayer | CanvasStagingArea) {
this.id = id;
this._parent = parent;
this._manager = parent._manager;
this._log = this._manager.buildLogger(this._getLoggingContext);
}
/**
* Destroy the object's konva nodes.
*/
abstract destroy(): void;
/**
* Set the visibility of the object's konva nodes.
*/
abstract setVisibility(isVisible: boolean): void;
/**
* Get a serializable representation of the object.
*/
abstract repr(): JSONObject;
/**
* Get the logging context for this object.
* @param extra Extra data to merge into the context
* @returns The logging context for this object
*/
_getLoggingContext = (extra?: Record<string, unknown>) => {
return {
...this._parent._getLoggingContext(),
objectId: this.id,
...extra,
};
};
}

View File

@ -1,39 +1,32 @@
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import { deepClone } from 'common/util/deepClone';
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
import { CanvasObject } from 'features/controlLayers/konva/CanvasObject';
import type { RectShape } from 'features/controlLayers/store/types';
import Konva from 'konva';
export class CanvasRect {
export class CanvasRect extends CanvasObject {
static NAME_PREFIX = 'canvas-rect';
static GROUP_NAME = `${CanvasRect.NAME_PREFIX}_group`;
static RECT_NAME = `${CanvasRect.NAME_PREFIX}_rect`;
static TYPE = 'rect';
state: RectShape;
type = 'rect';
id: string;
konva: {
group: Konva.Group;
rect: Konva.Rect;
};
parent: CanvasLayer;
constructor(state: RectShape, parent: CanvasLayer) {
const { id, x, y, width, height, color } = state;
super(state.id, parent);
this._log.trace({ state }, 'Creating rect');
this.id = id;
this.parent = parent;
this.parent._log.trace(`Creating rect ${this.id}`);
const { x, y, width, height, color } = state;
this.konva = {
group: new Konva.Group({ name: CanvasRect.GROUP_NAME, listening: false }),
rect: new Konva.Rect({
name: CanvasRect.RECT_NAME,
id,
x,
y,
width,
@ -48,7 +41,7 @@ export class CanvasRect {
update(state: RectShape, force?: boolean): boolean {
if (this.state !== state || force) {
this.parent._log.trace(`Updating rect ${this.id}`);
this._log.trace({ state }, 'Updating rect');
const { x, y, width, height, color } = state;
this.konva.rect.setAttrs({
x,
@ -65,23 +58,20 @@ export class CanvasRect {
}
destroy() {
this.parent._log.trace(`Destroying rect ${this.id}`);
this._log.trace('Destroying rect');
this.konva.group.destroy();
}
show() {
this.konva.group.visible(true);
}
hide() {
this.konva.group.visible(false);
setVisibility(isVisible: boolean): void {
this._log.trace({ isVisible }, 'Setting rect visibility');
this.konva.group.visible(isVisible);
}
repr() {
return {
id: this.id,
type: this.type,
parent: this.parent.id,
type: CanvasRect.TYPE,
parent: this._parent.id,
state: deepClone(this.state),
};
}

View File

@ -1,29 +1,30 @@
import { CanvasEntity } from 'features/controlLayers/konva/CanvasEntity';
import { CanvasImage } from 'features/controlLayers/konva/CanvasImage';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import type { StagingAreaImage } from 'features/controlLayers/store/types';
import Konva from 'konva';
export class CanvasStagingArea {
export class CanvasStagingArea extends CanvasEntity {
static NAME_PREFIX = 'staging-area';
static GROUP_NAME = `${CanvasStagingArea.NAME_PREFIX}_group`;
type = 'staging_area';
konva: { group: Konva.Group };
image: CanvasImage | null;
selectedImage: StagingAreaImage | null;
manager: CanvasManager;
constructor(manager: CanvasManager) {
this.manager = manager;
super('staging-area', manager);
this.konva = { group: new Konva.Group({ name: CanvasStagingArea.GROUP_NAME, listening: false }) };
this.image = null;
this.selectedImage = null;
}
async render() {
const session = this.manager.stateApi.getSession();
const bboxRect = this.manager.stateApi.getBbox().rect;
const shouldShowStagedImage = this.manager.stateApi.getShouldShowStagedImage();
const session = this._manager.stateApi.getSession();
const bboxRect = this._manager.stateApi.getBbox().rect;
const shouldShowStagedImage = this._manager.stateApi.getShouldShowStagedImage();
this.selectedImage = session.stagedImages[session.selectedStagedImageIndex] ?? null;
@ -32,34 +33,45 @@ export class CanvasStagingArea {
if (!this.image) {
const { image_name, width, height } = imageDTO;
this.image = new CanvasImage({
id: 'staging-area-image',
type: 'image',
x: 0,
y: 0,
width,
height,
filters: [],
image: {
name: image_name,
this.image = new CanvasImage(
{
id: 'staging-area-image',
type: 'image',
x: 0,
y: 0,
width,
height,
filters: [],
image: {
name: image_name,
width,
height,
},
},
});
this
);
this.konva.group.add(this.image.konva.group);
}
if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) {
this.image.image?.width(imageDTO.width);
this.image.image?.height(imageDTO.height);
this.image.konva.image?.width(imageDTO.width);
this.image.konva.image?.height(imageDTO.height);
this.image.konva.group.x(bboxRect.x + offsetX);
this.image.konva.group.y(bboxRect.y + offsetY);
await this.image.updateImageSource(imageDTO.image_name);
this.manager.stateApi.resetLastProgressEvent();
this._manager.stateApi.resetLastProgressEvent();
}
this.image.konva.group.visible(shouldShowStagedImage);
} else {
this.image?.konva.group.visible(false);
}
}
repr() {
return {
id: this.id,
type: this.type,
selectedImage: this.selectedImage,
};
}
}

View File

@ -248,6 +248,9 @@ export class CanvasStateApi {
getIsSelected = (id: string) => {
return this.getSelectedEntity()?.id === id;
};
getLogLevel = () => {
return this.store.getState().system.consoleLogLevel;
};
// Read-only state, derived from nanostores
resetLastProgressEvent = () => {

View File

@ -1,5 +1,5 @@
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getScaledFlooredCursorPosition, nanoid } from 'features/controlLayers/konva/util';
import { getObjectId, getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util';
import type {
CanvasV2State,
Coordinate,
@ -14,7 +14,6 @@ import type { KonvaEventObject } from 'konva/lib/Node';
import { clamp } from 'lodash-es';
import { BRUSH_SPACING_TARGET_SCALE, CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from './constants';
import { getBrushLineId, getEraserLineId, getRectShapeId } from './naming';
/**
* Updates the last cursor position atom with the current cursor position, returning the new position or `null` if the
@ -187,7 +186,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
await selectedEntityAdapter.finalizeDrawingBuffer();
}
await selectedEntityAdapter.setDrawingBuffer({
id: getBrushLineId(selectedEntityAdapter.id, nanoid(), true),
id: getObjectId('brush_line', true),
type: 'brush_line',
points: [
// The last point of the last line is already normalized to the entity's coordinates
@ -205,7 +204,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
await selectedEntityAdapter.finalizeDrawingBuffer();
}
await selectedEntityAdapter.setDrawingBuffer({
id: getBrushLineId(selectedEntityAdapter.id, nanoid(), true),
id: getObjectId('brush_line', true),
type: 'brush_line',
points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y],
strokeWidth: toolState.brush.width,
@ -224,7 +223,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
await selectedEntityAdapter.finalizeDrawingBuffer();
}
await selectedEntityAdapter.setDrawingBuffer({
id: getEraserLineId(selectedEntityAdapter.id, nanoid(), true),
id: getObjectId('eraser_line', true),
type: 'eraser_line',
points: [
// The last point of the last line is already normalized to the entity's coordinates
@ -241,7 +240,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
await selectedEntityAdapter.finalizeDrawingBuffer();
}
await selectedEntityAdapter.setDrawingBuffer({
id: getEraserLineId(selectedEntityAdapter.id, nanoid(), true),
id: getObjectId('eraser_line', true),
type: 'eraser_line',
points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y],
strokeWidth: toolState.eraser.width,
@ -256,7 +255,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
await selectedEntityAdapter.finalizeDrawingBuffer();
}
await selectedEntityAdapter.setDrawingBuffer({
id: getRectShapeId(selectedEntityAdapter.id, nanoid(), true),
id: getObjectId('rect_shape', true),
type: 'rect_shape',
x: pos.x - selectedEntity.position.x,
y: pos.y - selectedEntity.position.y,
@ -356,7 +355,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
await selectedEntityAdapter.finalizeDrawingBuffer();
}
await selectedEntityAdapter.setDrawingBuffer({
id: getBrushLineId(selectedEntityAdapter.id, nanoid(), true),
id: getObjectId('brush_line', true),
type: 'brush_line',
points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y],
strokeWidth: toolState.brush.width,
@ -388,7 +387,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
await selectedEntityAdapter.finalizeDrawingBuffer();
}
await selectedEntityAdapter.setDrawingBuffer({
id: getEraserLineId(selectedEntityAdapter.id, nanoid(), true),
id: getObjectId('eraser_line', true),
type: 'eraser_line',
points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y],
strokeWidth: toolState.eraser.width,

View File

@ -6,13 +6,13 @@
export const getRGId = (entityId: string) => `region_${entityId}`;
export const getLayerId = (entityId: string) => `layer_${entityId}`;
export const getBrushLineId = (entityId: string, lineId: string, isBuffer?: boolean) =>
`${entityId}.${isBuffer ? 'buffer_' : ''}brush_line_${lineId}`;
`${isBuffer ? 'buffer_' : ''}brush_line_${lineId}`;
export const getEraserLineId = (entityId: string, lineId: string, isBuffer?: boolean) =>
`${entityId}.${isBuffer ? 'buffer_' : ''}eraser_line_${lineId}`;
`${isBuffer ? 'buffer_' : ''}eraser_line_${lineId}`;
export const getRectShapeId = (entityId: string, rectId: string, isBuffer?: boolean) =>
`${entityId}.${isBuffer ? 'buffer_' : ''}rect_${rectId}`;
export const getImageObjectId = (entityId: string, imageId: string) => `${entityId}.image_${imageId}`;
export const getObjectGroupId = (entityId: string, groupId: string) => `${entityId}.objectGroup_${groupId}`;
`${isBuffer ? 'buffer_' : ''}rect_${rectId}`;
export const getImageObjectId = (entityId: string, imageId: string) => `image_${imageId}`;
export const getObjectGroupId = (entityId: string, groupId: string) => `objectGroup_${groupId}`;
export const getLayerBboxId = (entityId: string) => `${entityId}.bbox`;
export const getCAId = (entityId: string) => `control_adapter_${entityId}`;
export const getIPAId = (entityId: string) => `ip_adapter_${entityId}`;

View File

@ -1,12 +1,12 @@
import { getImageDataTransparency } from 'common/util/arrayBuffer';
import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import type { GenerationMode, Rect, RgbaColor } from 'features/controlLayers/store/types';
import type { GenerationMode, Rect, RenderableObject, 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';
import type { Vector2d } from 'konva/lib/types';
import { customAlphabet, urlAlphabet } from 'nanoid';
import { customAlphabet } from 'nanoid';
import type { ImageDTO } from 'services/api/types';
import { assert } from 'tsafe';
@ -575,4 +575,19 @@ export function loadImage(src: string, imageEl?: HTMLImageElement): Promise<HTML
});
}
export const nanoid = customAlphabet(urlAlphabet, 10);
/**
* Generates a random alphanumeric string of length 10. Probably not secure at all.
*/
export const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 10);
export function getPrefixedId(prefix: string): string {
return `${prefix}:${nanoid()}`;
}
export function getObjectId(type: RenderableObject['type'], isBuffer?: boolean): string {
if (isBuffer) {
return getPrefixedId(`buffer_${type}`);
} else {
return getPrefixedId(type);
}
}

View File

@ -207,7 +207,6 @@ export const {
bboxSizeOptimized,
// layers
layerAdded,
layerAddedFromStagingArea,
layerRecalled,
layerDeleted,
layerReset,

View File

@ -162,7 +162,7 @@ export const controlAdaptersReducers = {
ca.bboxNeedsUpdate = true;
ca.isEnabled = true;
if (imageDTO) {
const newImageObject = imageDTOToImageObject(id, objectId, imageDTO, { filters: ca.filters });
const newImageObject = imageDTOToImageObject(imageDTO, { filters: ca.filters });
if (isEqual(newImageObject, ca.imageObject)) {
return;
}
@ -185,9 +185,7 @@ export const controlAdaptersReducers = {
ca.bbox = null;
ca.bboxNeedsUpdate = true;
ca.isEnabled = true;
ca.processedImageObject = imageDTO
? imageDTOToImageObject(id, objectId, imageDTO, { filters: ca.filters })
: null;
ca.processedImageObject = imageDTO ? imageDTOToImageObject(imageDTO, { filters: ca.filters }) : null;
},
prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }),
},

View File

@ -25,7 +25,7 @@ export const initialImageReducers = {
if (!state.initialImage) {
return;
}
const newImageObject = imageDTOToImageObject('initial_image', 'initial_image_object', imageDTO);
const newImageObject = imageDTOToImageObject(imageDTO);
if (isEqual(newImageObject, state.initialImage.imageObject)) {
return;
}

View File

@ -4,13 +4,7 @@ import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
import type {
CanvasV2State,
CLIPVisionModelV2,
IPAdapterConfig,
IPAdapterEntity,
IPMethodV2,
} from './types';
import type { CanvasV2State, CLIPVisionModelV2, IPAdapterConfig, IPAdapterEntity, IPMethodV2 } from './types';
import { imageDTOToImageObject } from './types';
export const selectIPA = (state: CanvasV2State, id: string) => state.ipAdapters.entities.find((ipa) => ipa.id === id);
@ -61,7 +55,7 @@ export const ipAdaptersReducers = {
if (!ipa) {
return;
}
ipa.imageObject = imageDTO ? imageDTOToImageObject(id, objectId, imageDTO) : null;
ipa.imageObject = imageDTO ? imageDTOToImageObject(imageDTO) : null;
},
prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }),
},

View File

@ -1,7 +1,8 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { nanoid } from 'features/controlLayers/konva/util';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import type { IRect } from 'konva/lib/types';
import { merge } from 'lodash-es';
import type { ImageDTO } from 'services/api/types';
import { assert } from 'tsafe';
@ -16,7 +17,6 @@ import type {
PositionChangedArg,
RectShape,
ScaleChangedArg,
StagingAreaImage,
} from './types';
import { imageDTOToImageObject, imageDTOToImageWithDims } from './types';
@ -29,42 +29,23 @@ export const selectLayerOrThrow = (state: CanvasV2State, id: string) => {
export const layersReducers = {
layerAdded: {
reducer: (state, action: PayloadAction<{ id: string }>) => {
reducer: (state, action: PayloadAction<{ id: string; overrides?: Partial<LayerEntity> }>) => {
const { id } = action.payload;
state.layers.entities.push({
const layer: LayerEntity = {
id,
type: 'layer',
isEnabled: true,
objects: [],
opacity: 1,
position: { x: 0, y: 0 },
});
};
merge(layer, action.payload.overrides);
state.layers.entities.push(layer);
state.selectedEntityIdentifier = { type: 'layer', id };
state.layers.imageCache = null;
},
prepare: () => ({ payload: { id: nanoid() } }),
},
layerAddedFromStagingArea: {
reducer: (
state,
action: PayloadAction<{ id: string; objectId: string; stagingAreaImage: StagingAreaImage; position: Coordinate }>
) => {
const { id, objectId, stagingAreaImage, position } = action.payload;
const { imageDTO, offsetX, offsetY } = stagingAreaImage;
const imageObject = imageDTOToImageObject(id, objectId, imageDTO);
state.layers.entities.push({
id,
type: 'layer',
isEnabled: true,
objects: [imageObject],
opacity: 1,
position: { x: position.x + offsetX, y: position.y + offsetY },
});
state.selectedEntityIdentifier = { type: 'layer', id };
state.layers.imageCache = null;
},
prepare: (payload: { stagingAreaImage: StagingAreaImage; position: Coordinate }) => ({
payload: { ...payload, id: nanoid(), objectId: nanoid() },
prepare: (payload: { overrides?: Partial<LayerEntity> }) => ({
payload: { ...payload, id: getPrefixedId('layer') },
}),
},
layerRecalled: (state, action: PayloadAction<{ data: LayerEntity }>) => {
@ -227,27 +208,22 @@ export const layersReducers = {
layer.position.y = Math.round(position.y);
state.layers.imageCache = null;
},
layerImageAdded: {
reducer: (
state,
action: PayloadAction<ImageObjectAddedArg & { objectId: string; pos?: { x: number; y: number } }>
) => {
const { id, objectId, imageDTO, pos } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
const imageObject = imageDTOToImageObject(id, objectId, imageDTO);
if (pos) {
imageObject.x = pos.x;
imageObject.y = pos.y;
}
layer.objects.push(imageObject);
state.layers.imageCache = null;
},
prepare: (payload: ImageObjectAddedArg & { pos?: { x: number; y: number } }) => ({
payload: { ...payload, objectId: nanoid() },
}),
layerImageAdded: (
state,
action: PayloadAction<ImageObjectAddedArg & { objectId: string; pos?: { x: number; y: number } }>
) => {
const { id, imageDTO, pos } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
const imageObject = imageDTOToImageObject(imageDTO);
if (pos) {
imageObject.x = pos.x;
imageObject.y = pos.y;
}
layer.objects.push(imageObject);
state.layers.imageCache = null;
},
layerImageCacheChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO | null }>) => {
const { imageDTO } = action.payload;

View File

@ -2,7 +2,7 @@ import type { CanvasControlAdapter } from 'features/controlLayers/konva/CanvasCo
import { CanvasInpaintMask } from 'features/controlLayers/konva/CanvasInpaintMask';
import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
import { CanvasRegion } from 'features/controlLayers/konva/CanvasRegion';
import { getImageObjectId } from 'features/controlLayers/konva/naming';
import { getObjectId } from 'features/controlLayers/konva/util';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { AspectRatioState } from 'features/parameters/components/DocumentSize/types';
import type {
@ -777,15 +777,10 @@ export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO)
height,
});
export const imageDTOToImageObject = (
entityId: string,
objectId: string,
imageDTO: ImageDTO,
overrides?: Partial<ImageObject>
): ImageObject => {
export const imageDTOToImageObject = (imageDTO: ImageDTO, overrides?: Partial<ImageObject>): ImageObject => {
const { width, height, image_name } = imageDTO;
return {
id: getImageObjectId(entityId, objectId),
id: getObjectId('image'),
type: 'image',
x: 0,
y: 0,