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 type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { import {
$lastProgressEvent, $lastProgressEvent,
layerAddedFromStagingArea, layerAdded,
sessionStagingAreaImageAccepted, sessionStagingAreaImageAccepted,
sessionStagingAreaReset, sessionStagingAreaReset,
} from 'features/controlLayers/store/canvasV2Slice'; } 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 { toast } from 'features/toast/toast';
import { t } from 'i18next'; import { t } from 'i18next';
import { queueApi } from 'services/api/endpoints/queue'; import { queueApi } from 'services/api/endpoints/queue';
@ -50,7 +52,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => {
startAppListening({ startAppListening({
actionCreator: sessionStagingAreaImageAccepted, actionCreator: sessionStagingAreaImageAccepted,
effect: async (action, api) => { effect: (action, api) => {
const { index } = action.payload; const { index } = action.payload;
const state = api.getState(); const state = api.getState();
const stagingAreaImage = state.canvasV2.session.stagedImages[index]; const stagingAreaImage = state.canvasV2.session.stagedImages[index];
@ -58,7 +60,14 @@ export const addStagingListeners = (startAppListening: AppStartListening) => {
assert(stagingAreaImage, 'No staged image found to accept'); assert(stagingAreaImage, 'No staged image found to accept');
const { x, y } = state.canvasV2.bbox.rect; 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()); api.dispatch(sessionStagingAreaReset());
}, },
}); });

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import type { Store } from '@reduxjs/toolkit'; import type { Store } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger'; import { logger } from 'app/logging/logger';
import type { RootState } from 'app/store/store'; import type { RootState } from 'app/store/store';
import type { JSONObject } from 'common/types';
import { CanvasInitialImage } from 'features/controlLayers/konva/CanvasInitialImage'; import { CanvasInitialImage } from 'features/controlLayers/konva/CanvasInitialImage';
import { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview'; import { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview';
import { import {
@ -107,7 +108,15 @@ export class CanvasManager {
this._prevState = this.stateApi.getState(); this._prevState = this.stateApi.getState();
this._isFirstRender = true; 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.workerLog = logger('worker');
this.util = { this.util = {
@ -173,11 +182,6 @@ export class CanvasManager {
this._isDebugging = false; 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) { requestBbox(data: Omit<GetBboxTask['data'], 'id'>, onComplete: (extents: Extents | null) => void) {
const id = nanoid(); const id = nanoid();
const task: GetBboxTask = { const task: GetBboxTask = {
@ -330,7 +334,6 @@ export class CanvasManager {
for (const canvasLayer of this.layers.values()) { for (const canvasLayer of this.layers.values()) {
if (!state.layers.entities.find((l) => l.id === canvasLayer.id)) { if (!state.layers.entities.find((l) => l.id === canvasLayer.id)) {
this.log.debug(`Destroying deleted layer ${canvasLayer.id}`);
await canvasLayer.destroy(); await canvasLayer.destroy();
this.layers.delete(canvasLayer.id); this.layers.delete(canvasLayer.id);
} }
@ -339,7 +342,6 @@ export class CanvasManager {
for (const entityState of state.layers.entities) { for (const entityState of state.layers.entities) {
let adapter = this.layers.get(entityState.id); let adapter = this.layers.get(entityState.id);
if (!adapter) { if (!adapter) {
this.log.debug(`Creating layer layer ${entityState.id}`);
adapter = new CanvasLayer(entityState, this); adapter = new CanvasLayer(entityState, this);
this.layers.set(adapter.id, adapter); this.layers.set(adapter.id, adapter);
this.stage.add(adapter.konva.layer); 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() { logDebugInfo() {
// eslint-disable-next-line no-console
console.log(this); console.log(this);
for (const layer of this.layers.values()) { for (const layer of this.layers.values()) {
// eslint-disable-next-line no-console
console.log(layer); 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 { rgbaColorToString } from 'common/util/colorCodeTransformers';
import { deepClone } from 'common/util/deepClone'; import { deepClone } from 'common/util/deepClone';
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
import { CanvasObject } from 'features/controlLayers/konva/CanvasObject';
import type { RectShape } from 'features/controlLayers/store/types'; import type { RectShape } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
export class CanvasRect { export class CanvasRect extends CanvasObject {
static NAME_PREFIX = 'canvas-rect'; static NAME_PREFIX = 'canvas-rect';
static GROUP_NAME = `${CanvasRect.NAME_PREFIX}_group`; static GROUP_NAME = `${CanvasRect.NAME_PREFIX}_group`;
static RECT_NAME = `${CanvasRect.NAME_PREFIX}_rect`; static RECT_NAME = `${CanvasRect.NAME_PREFIX}_rect`;
static TYPE = 'rect';
state: RectShape; state: RectShape;
type = 'rect';
id: string;
konva: { konva: {
group: Konva.Group; group: Konva.Group;
rect: Konva.Rect; rect: Konva.Rect;
}; };
parent: CanvasLayer;
constructor(state: RectShape, 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; const { x, y, width, height, color } = state;
this.parent = parent;
this.parent._log.trace(`Creating rect ${this.id}`);
this.konva = { this.konva = {
group: new Konva.Group({ name: CanvasRect.GROUP_NAME, listening: false }), group: new Konva.Group({ name: CanvasRect.GROUP_NAME, listening: false }),
rect: new Konva.Rect({ rect: new Konva.Rect({
name: CanvasRect.RECT_NAME, name: CanvasRect.RECT_NAME,
id,
x, x,
y, y,
width, width,
@ -48,7 +41,7 @@ export class CanvasRect {
update(state: RectShape, force?: boolean): boolean { update(state: RectShape, force?: boolean): boolean {
if (this.state !== state || force) { 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; const { x, y, width, height, color } = state;
this.konva.rect.setAttrs({ this.konva.rect.setAttrs({
x, x,
@ -65,23 +58,20 @@ export class CanvasRect {
} }
destroy() { destroy() {
this.parent._log.trace(`Destroying rect ${this.id}`); this._log.trace('Destroying rect');
this.konva.group.destroy(); this.konva.group.destroy();
} }
show() { setVisibility(isVisible: boolean): void {
this.konva.group.visible(true); this._log.trace({ isVisible }, 'Setting rect visibility');
} this.konva.group.visible(isVisible);
hide() {
this.konva.group.visible(false);
} }
repr() { repr() {
return { return {
id: this.id, id: this.id,
type: this.type, type: CanvasRect.TYPE,
parent: this.parent.id, parent: this._parent.id,
state: deepClone(this.state), 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 { CanvasImage } from 'features/controlLayers/konva/CanvasImage';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import type { StagingAreaImage } from 'features/controlLayers/store/types'; import type { StagingAreaImage } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
export class CanvasStagingArea { export class CanvasStagingArea extends CanvasEntity {
static NAME_PREFIX = 'staging-area'; static NAME_PREFIX = 'staging-area';
static GROUP_NAME = `${CanvasStagingArea.NAME_PREFIX}_group`; static GROUP_NAME = `${CanvasStagingArea.NAME_PREFIX}_group`;
type = 'staging_area';
konva: { group: Konva.Group }; konva: { group: Konva.Group };
image: CanvasImage | null; image: CanvasImage | null;
selectedImage: StagingAreaImage | null; selectedImage: StagingAreaImage | null;
manager: CanvasManager;
constructor(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.konva = { group: new Konva.Group({ name: CanvasStagingArea.GROUP_NAME, listening: false }) };
this.image = null; this.image = null;
this.selectedImage = null; this.selectedImage = null;
} }
async render() { async render() {
const session = this.manager.stateApi.getSession(); const session = this._manager.stateApi.getSession();
const bboxRect = this.manager.stateApi.getBbox().rect; const bboxRect = this._manager.stateApi.getBbox().rect;
const shouldShowStagedImage = this.manager.stateApi.getShouldShowStagedImage(); const shouldShowStagedImage = this._manager.stateApi.getShouldShowStagedImage();
this.selectedImage = session.stagedImages[session.selectedStagedImageIndex] ?? null; this.selectedImage = session.stagedImages[session.selectedStagedImageIndex] ?? null;
@ -32,34 +33,45 @@ export class CanvasStagingArea {
if (!this.image) { if (!this.image) {
const { image_name, width, height } = imageDTO; const { image_name, width, height } = imageDTO;
this.image = new CanvasImage({ this.image = new CanvasImage(
id: 'staging-area-image', {
type: 'image', id: 'staging-area-image',
x: 0, type: 'image',
y: 0, x: 0,
width, y: 0,
height,
filters: [],
image: {
name: image_name,
width, width,
height, height,
filters: [],
image: {
name: image_name,
width,
height,
},
}, },
}); this
);
this.konva.group.add(this.image.konva.group); this.konva.group.add(this.image.konva.group);
} }
if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) { if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) {
this.image.image?.width(imageDTO.width); this.image.konva.image?.width(imageDTO.width);
this.image.image?.height(imageDTO.height); this.image.konva.image?.height(imageDTO.height);
this.image.konva.group.x(bboxRect.x + offsetX); this.image.konva.group.x(bboxRect.x + offsetX);
this.image.konva.group.y(bboxRect.y + offsetY); this.image.konva.group.y(bboxRect.y + offsetY);
await this.image.updateImageSource(imageDTO.image_name); await this.image.updateImageSource(imageDTO.image_name);
this.manager.stateApi.resetLastProgressEvent(); this._manager.stateApi.resetLastProgressEvent();
} }
this.image.konva.group.visible(shouldShowStagedImage); this.image.konva.group.visible(shouldShowStagedImage);
} else { } else {
this.image?.konva.group.visible(false); 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) => { getIsSelected = (id: string) => {
return this.getSelectedEntity()?.id === id; return this.getSelectedEntity()?.id === id;
}; };
getLogLevel = () => {
return this.store.getState().system.consoleLogLevel;
};
// Read-only state, derived from nanostores // Read-only state, derived from nanostores
resetLastProgressEvent = () => { resetLastProgressEvent = () => {

View File

@ -1,5 +1,5 @@
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; 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 { import type {
CanvasV2State, CanvasV2State,
Coordinate, Coordinate,
@ -14,7 +14,6 @@ import type { KonvaEventObject } from 'konva/lib/Node';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import { BRUSH_SPACING_TARGET_SCALE, CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from './constants'; 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 * 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.finalizeDrawingBuffer();
} }
await selectedEntityAdapter.setDrawingBuffer({ await selectedEntityAdapter.setDrawingBuffer({
id: getBrushLineId(selectedEntityAdapter.id, nanoid(), true), id: getObjectId('brush_line', true),
type: 'brush_line', type: 'brush_line',
points: [ points: [
// The last point of the last line is already normalized to the entity's coordinates // 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.finalizeDrawingBuffer();
} }
await selectedEntityAdapter.setDrawingBuffer({ await selectedEntityAdapter.setDrawingBuffer({
id: getBrushLineId(selectedEntityAdapter.id, nanoid(), true), id: getObjectId('brush_line', true),
type: 'brush_line', type: 'brush_line',
points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y],
strokeWidth: toolState.brush.width, strokeWidth: toolState.brush.width,
@ -224,7 +223,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
await selectedEntityAdapter.finalizeDrawingBuffer(); await selectedEntityAdapter.finalizeDrawingBuffer();
} }
await selectedEntityAdapter.setDrawingBuffer({ await selectedEntityAdapter.setDrawingBuffer({
id: getEraserLineId(selectedEntityAdapter.id, nanoid(), true), id: getObjectId('eraser_line', true),
type: 'eraser_line', type: 'eraser_line',
points: [ points: [
// The last point of the last line is already normalized to the entity's coordinates // 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.finalizeDrawingBuffer();
} }
await selectedEntityAdapter.setDrawingBuffer({ await selectedEntityAdapter.setDrawingBuffer({
id: getEraserLineId(selectedEntityAdapter.id, nanoid(), true), id: getObjectId('eraser_line', true),
type: 'eraser_line', type: 'eraser_line',
points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y],
strokeWidth: toolState.eraser.width, strokeWidth: toolState.eraser.width,
@ -256,7 +255,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
await selectedEntityAdapter.finalizeDrawingBuffer(); await selectedEntityAdapter.finalizeDrawingBuffer();
} }
await selectedEntityAdapter.setDrawingBuffer({ await selectedEntityAdapter.setDrawingBuffer({
id: getRectShapeId(selectedEntityAdapter.id, nanoid(), true), id: getObjectId('rect_shape', true),
type: 'rect_shape', type: 'rect_shape',
x: pos.x - selectedEntity.position.x, x: pos.x - selectedEntity.position.x,
y: pos.y - selectedEntity.position.y, y: pos.y - selectedEntity.position.y,
@ -356,7 +355,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
await selectedEntityAdapter.finalizeDrawingBuffer(); await selectedEntityAdapter.finalizeDrawingBuffer();
} }
await selectedEntityAdapter.setDrawingBuffer({ await selectedEntityAdapter.setDrawingBuffer({
id: getBrushLineId(selectedEntityAdapter.id, nanoid(), true), id: getObjectId('brush_line', true),
type: 'brush_line', type: 'brush_line',
points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y],
strokeWidth: toolState.brush.width, strokeWidth: toolState.brush.width,
@ -388,7 +387,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
await selectedEntityAdapter.finalizeDrawingBuffer(); await selectedEntityAdapter.finalizeDrawingBuffer();
} }
await selectedEntityAdapter.setDrawingBuffer({ await selectedEntityAdapter.setDrawingBuffer({
id: getEraserLineId(selectedEntityAdapter.id, nanoid(), true), id: getObjectId('eraser_line', true),
type: 'eraser_line', type: 'eraser_line',
points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y],
strokeWidth: toolState.eraser.width, strokeWidth: toolState.eraser.width,

View File

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

View File

@ -1,12 +1,12 @@
import { getImageDataTransparency } from 'common/util/arrayBuffer'; import { getImageDataTransparency } from 'common/util/arrayBuffer';
import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; 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 { isValidLayer } from 'features/nodes/util/graph/generation/addLayers';
import Konva from 'konva'; import Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node'; import type { KonvaEventObject } from 'konva/lib/Node';
import type { Vector2d } from 'konva/lib/types'; import type { Vector2d } from 'konva/lib/types';
import { customAlphabet, urlAlphabet } from 'nanoid'; import { customAlphabet } from 'nanoid';
import type { ImageDTO } from 'services/api/types'; import type { ImageDTO } from 'services/api/types';
import { assert } from 'tsafe'; 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, bboxSizeOptimized,
// layers // layers
layerAdded, layerAdded,
layerAddedFromStagingArea,
layerRecalled, layerRecalled,
layerDeleted, layerDeleted,
layerReset, layerReset,

View File

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

View File

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

View File

@ -4,13 +4,7 @@ import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import type { import type { CanvasV2State, CLIPVisionModelV2, IPAdapterConfig, IPAdapterEntity, IPMethodV2 } from './types';
CanvasV2State,
CLIPVisionModelV2,
IPAdapterConfig,
IPAdapterEntity,
IPMethodV2,
} from './types';
import { imageDTOToImageObject } from './types'; import { imageDTOToImageObject } from './types';
export const selectIPA = (state: CanvasV2State, id: string) => state.ipAdapters.entities.find((ipa) => ipa.id === id); export const selectIPA = (state: CanvasV2State, id: string) => state.ipAdapters.entities.find((ipa) => ipa.id === id);
@ -61,7 +55,7 @@ export const ipAdaptersReducers = {
if (!ipa) { if (!ipa) {
return; 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() } }), 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 type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; 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 type { IRect } from 'konva/lib/types';
import { merge } from 'lodash-es';
import type { ImageDTO } from 'services/api/types'; import type { ImageDTO } from 'services/api/types';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
@ -16,7 +17,6 @@ import type {
PositionChangedArg, PositionChangedArg,
RectShape, RectShape,
ScaleChangedArg, ScaleChangedArg,
StagingAreaImage,
} from './types'; } from './types';
import { imageDTOToImageObject, imageDTOToImageWithDims } from './types'; import { imageDTOToImageObject, imageDTOToImageWithDims } from './types';
@ -29,42 +29,23 @@ export const selectLayerOrThrow = (state: CanvasV2State, id: string) => {
export const layersReducers = { export const layersReducers = {
layerAdded: { layerAdded: {
reducer: (state, action: PayloadAction<{ id: string }>) => { reducer: (state, action: PayloadAction<{ id: string; overrides?: Partial<LayerEntity> }>) => {
const { id } = action.payload; const { id } = action.payload;
state.layers.entities.push({ const layer: LayerEntity = {
id, id,
type: 'layer', type: 'layer',
isEnabled: true, isEnabled: true,
objects: [], objects: [],
opacity: 1, opacity: 1,
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
}); };
merge(layer, action.payload.overrides);
state.layers.entities.push(layer);
state.selectedEntityIdentifier = { type: 'layer', id }; state.selectedEntityIdentifier = { type: 'layer', id };
state.layers.imageCache = null; state.layers.imageCache = null;
}, },
prepare: () => ({ payload: { id: nanoid() } }), prepare: (payload: { overrides?: Partial<LayerEntity> }) => ({
}, payload: { ...payload, id: getPrefixedId('layer') },
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() },
}), }),
}, },
layerRecalled: (state, action: PayloadAction<{ data: LayerEntity }>) => { layerRecalled: (state, action: PayloadAction<{ data: LayerEntity }>) => {
@ -227,27 +208,22 @@ export const layersReducers = {
layer.position.y = Math.round(position.y); layer.position.y = Math.round(position.y);
state.layers.imageCache = null; state.layers.imageCache = null;
}, },
layerImageAdded: { layerImageAdded: (
reducer: ( state,
state, action: PayloadAction<ImageObjectAddedArg & { objectId: string; pos?: { x: number; y: number } }>
action: PayloadAction<ImageObjectAddedArg & { objectId: string; pos?: { x: number; y: number } }> ) => {
) => { const { id, imageDTO, pos } = action.payload;
const { id, objectId, imageDTO, pos } = action.payload; const layer = selectLayer(state, id);
const layer = selectLayer(state, id); if (!layer) {
if (!layer) { return;
return; }
} const imageObject = imageDTOToImageObject(imageDTO);
const imageObject = imageDTOToImageObject(id, objectId, imageDTO); if (pos) {
if (pos) { imageObject.x = pos.x;
imageObject.x = pos.x; imageObject.y = pos.y;
imageObject.y = pos.y; }
} layer.objects.push(imageObject);
layer.objects.push(imageObject); state.layers.imageCache = null;
state.layers.imageCache = null;
},
prepare: (payload: ImageObjectAddedArg & { pos?: { x: number; y: number } }) => ({
payload: { ...payload, objectId: nanoid() },
}),
}, },
layerImageCacheChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO | null }>) => { layerImageCacheChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO | null }>) => {
const { imageDTO } = action.payload; 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 { CanvasInpaintMask } from 'features/controlLayers/konva/CanvasInpaintMask';
import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
import { CanvasRegion } from 'features/controlLayers/konva/CanvasRegion'; 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 { zModelIdentifierField } from 'features/nodes/types/common';
import type { AspectRatioState } from 'features/parameters/components/DocumentSize/types'; import type { AspectRatioState } from 'features/parameters/components/DocumentSize/types';
import type { import type {
@ -777,15 +777,10 @@ export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO)
height, height,
}); });
export const imageDTOToImageObject = ( export const imageDTOToImageObject = (imageDTO: ImageDTO, overrides?: Partial<ImageObject>): ImageObject => {
entityId: string,
objectId: string,
imageDTO: ImageDTO,
overrides?: Partial<ImageObject>
): ImageObject => {
const { width, height, image_name } = imageDTO; const { width, height, image_name } = imageDTO;
return { return {
id: getImageObjectId(entityId, objectId), id: getObjectId('image'),
type: 'image', type: 'image',
x: 0, x: 0,
y: 0, y: 0,