mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): organize konva state and files
This commit is contained in:
parent
0b5d20c9f0
commit
47b94d563c
@ -1,12 +1,16 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { $isDebugging } from 'app/store/nanostores/isDebugging';
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay';
|
||||
import { initializeRenderer } from 'features/controlLayers/konva/renderers/renderer';
|
||||
import { KonvaNodeManager, setNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||
import Konva from 'konva';
|
||||
import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const log = logger('konva');
|
||||
|
||||
// This will log warnings when layers > 5 - maybe use `import.meta.env.MODE === 'development'` instead?
|
||||
Konva.showWarnings = false;
|
||||
|
||||
@ -15,7 +19,25 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
|
||||
const dpr = useDevicePixelRatio({ round: false });
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const cleanup = initializeRenderer(store, stage, container);
|
||||
/**
|
||||
* Logs a message to the console if debugging is enabled.
|
||||
*/
|
||||
const logIfDebugging = (message: string) => {
|
||||
if ($isDebugging.get()) {
|
||||
log.debug(message);
|
||||
}
|
||||
};
|
||||
|
||||
logIfDebugging('Initializing renderer');
|
||||
if (!container) {
|
||||
// Nothing to clean up
|
||||
logIfDebugging('No stage container, skipping initialization');
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const manager = new KonvaNodeManager(stage, container, store, logIfDebugging);
|
||||
setNodeManager(manager);
|
||||
const cleanup = manager.initialize();
|
||||
return cleanup;
|
||||
}, [asPreview, container, stage, store]);
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { getArbitraryBaseColor } from '@invoke-ai/ui-library';
|
||||
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||
import Konva from 'konva';
|
||||
|
||||
const baseGridLineColor = getArbitraryBaseColor(27);
|
||||
@ -30,19 +31,21 @@ const getGridSpacing = (scale: number): number => {
|
||||
|
||||
export class CanvasBackground {
|
||||
layer: Konva.Layer;
|
||||
manager: KonvaNodeManager;
|
||||
|
||||
constructor() {
|
||||
constructor(manager: KonvaNodeManager) {
|
||||
this.manager = manager;
|
||||
this.layer = new Konva.Layer({ listening: false });
|
||||
}
|
||||
|
||||
renderBackground(stage: Konva.Stage): void {
|
||||
renderBackground() {
|
||||
this.layer.zIndex(0);
|
||||
const scale = stage.scaleX();
|
||||
const scale = this.manager.stage.scaleX();
|
||||
const gridSpacing = getGridSpacing(scale);
|
||||
const x = stage.x();
|
||||
const y = stage.y();
|
||||
const width = stage.width();
|
||||
const height = stage.height();
|
||||
const x = this.manager.stage.x();
|
||||
const y = this.manager.stage.y();
|
||||
const width = this.manager.stage.width();
|
||||
const height = this.manager.stage.height();
|
||||
const stageRect = {
|
||||
x1: 0,
|
||||
y1: 0,
|
@ -2,19 +2,19 @@ import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMult
|
||||
import {
|
||||
PREVIEW_GENERATION_BBOX_DUMMY_RECT,
|
||||
PREVIEW_GENERATION_BBOX_GROUP,
|
||||
PREVIEW_GENERATION_BBOX_TRANSFORMER
|
||||
PREVIEW_GENERATION_BBOX_TRANSFORMER,
|
||||
} from 'features/controlLayers/konva/naming';
|
||||
import type { CanvasV2State } from 'features/controlLayers/store/types';
|
||||
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||
import Konva from 'konva';
|
||||
import type { IRect } from 'konva/lib/types';
|
||||
import { atom } from 'nanostores';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
|
||||
export class CanvasBbox {
|
||||
group: Konva.Group;
|
||||
rect: Konva.Rect;
|
||||
transformer: Konva.Transformer;
|
||||
manager: KonvaNodeManager;
|
||||
|
||||
ALL_ANCHORS: string[] = [
|
||||
'top-left',
|
||||
@ -29,17 +29,11 @@ export class CanvasBbox {
|
||||
CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
||||
NO_ANCHORS: string[] = [];
|
||||
|
||||
constructor(
|
||||
getBbox: () => IRect,
|
||||
onBboxTransformed: (bbox: IRect) => void,
|
||||
getShiftKey: () => boolean,
|
||||
getCtrlKey: () => boolean,
|
||||
getMetaKey: () => boolean,
|
||||
getAltKey: () => boolean
|
||||
) {
|
||||
constructor(manager: KonvaNodeManager) {
|
||||
this.manager = manager;
|
||||
// Create a stash to hold onto the last aspect ratio of the bbox - this allows for locking the aspect ratio when
|
||||
// transforming the bbox.
|
||||
const bbox = getBbox();
|
||||
const bbox = this.manager.stateApi.getBbox();
|
||||
const $aspectRatioBuffer = atom(bbox.width / bbox.height);
|
||||
|
||||
// Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully
|
||||
@ -50,11 +44,11 @@ export class CanvasBbox {
|
||||
listening: false,
|
||||
strokeEnabled: false,
|
||||
draggable: true,
|
||||
...getBbox(),
|
||||
...this.manager.stateApi.getBbox(),
|
||||
});
|
||||
this.rect.on('dragmove', () => {
|
||||
const gridSize = getCtrlKey() || getMetaKey() ? 8 : 64;
|
||||
const oldBbox = getBbox();
|
||||
const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64;
|
||||
const oldBbox = this.manager.stateApi.getBbox();
|
||||
const newBbox: IRect = {
|
||||
...oldBbox,
|
||||
x: roundToMultiple(this.rect.x(), gridSize),
|
||||
@ -62,7 +56,7 @@ export class CanvasBbox {
|
||||
};
|
||||
this.rect.setAttrs(newBbox);
|
||||
if (oldBbox.x !== newBbox.x || oldBbox.y !== newBbox.y) {
|
||||
onBboxTransformed(newBbox);
|
||||
this.manager.stateApi.onBboxTransformed(newBbox);
|
||||
}
|
||||
});
|
||||
|
||||
@ -104,7 +98,7 @@ export class CanvasBbox {
|
||||
assert(stage, 'Stage must exist');
|
||||
|
||||
// We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid.
|
||||
const gridSize = getCtrlKey() || getMetaKey() ? 8 : 64;
|
||||
const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64;
|
||||
// Because we are working in absolute coordinates, we need to scale the grid size by the stage scale.
|
||||
const scaledGridSize = gridSize * stage.scaleX();
|
||||
// To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position.
|
||||
@ -129,10 +123,10 @@ export class CanvasBbox {
|
||||
return;
|
||||
}
|
||||
|
||||
const alt = getAltKey();
|
||||
const ctrl = getCtrlKey();
|
||||
const meta = getMetaKey();
|
||||
const shift = getShiftKey();
|
||||
const alt = this.manager.stateApi.getAltKey();
|
||||
const ctrl = this.manager.stateApi.getCtrlKey();
|
||||
const meta = this.manager.stateApi.getMetaKey();
|
||||
const shift = this.manager.stateApi.getShiftKey();
|
||||
|
||||
// Grid size depends on the modifier keys
|
||||
let gridSize = ctrl || meta ? 8 : 64;
|
||||
@ -141,7 +135,7 @@ export class CanvasBbox {
|
||||
// new dimensions so that each size scales in the correct increments and doesn't mis-place the bbox. For example, if
|
||||
// we snapped the width and height to 8px increments, the bbox would be mis-placed by 4px in the x and y axes.
|
||||
// Doubling the grid size ensures the bbox's coords remain aligned to the 8px/64px grid.
|
||||
if (getAltKey()) {
|
||||
if (this.manager.stateApi.getAltKey()) {
|
||||
gridSize = gridSize * 2;
|
||||
}
|
||||
|
||||
@ -196,7 +190,7 @@ export class CanvasBbox {
|
||||
this.rect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 });
|
||||
|
||||
// Update the bbox in internal state.
|
||||
onBboxTransformed(bbox);
|
||||
this.manager.stateApi.onBboxTransformed(bbox);
|
||||
|
||||
// Update the aspect ratio buffer whenever the shift key is not held - this allows for a nice UX where you can start
|
||||
// a transform, get the right aspect ratio, then hold shift to lock it in.
|
||||
@ -217,7 +211,10 @@ export class CanvasBbox {
|
||||
this.group.add(this.transformer);
|
||||
}
|
||||
|
||||
render(bbox: CanvasV2State['bbox'], toolState: CanvasV2State['tool']) {
|
||||
render() {
|
||||
const bbox = this.manager.stateApi.getBbox();
|
||||
const toolState = this.manager.stateApi.getToolState();
|
||||
|
||||
this.group.listening(toolState.selected === 'bbox');
|
||||
this.rect.setAttrs({
|
||||
x: bbox.x,
|
@ -1,6 +1,6 @@
|
||||
import { getArbitraryBaseColor } from '@invoke-ai/ui-library';
|
||||
import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants';
|
||||
import type { CanvasV2State, StageAttrs } from 'features/controlLayers/store/types';
|
||||
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||
import Konva from 'konva';
|
||||
|
||||
export class CanvasDocumentSizeOverlay {
|
||||
@ -8,8 +8,10 @@ export class CanvasDocumentSizeOverlay {
|
||||
outerRect: Konva.Rect;
|
||||
innerRect: Konva.Rect;
|
||||
padding: number;
|
||||
manager: KonvaNodeManager;
|
||||
|
||||
constructor(padding?: number) {
|
||||
constructor(manager: KonvaNodeManager, padding?: number) {
|
||||
this.manager = manager;
|
||||
this.padding = padding ?? DOCUMENT_FIT_PADDING_PX;
|
||||
this.group = new Konva.Group({ id: 'document_overlay_group', listening: false });
|
||||
this.outerRect = new Konva.Rect({
|
||||
@ -28,14 +30,15 @@ export class CanvasDocumentSizeOverlay {
|
||||
this.group.add(this.innerRect);
|
||||
}
|
||||
|
||||
render(stage: Konva.Stage, document: CanvasV2State['document']) {
|
||||
render() {
|
||||
const document = this.manager.stateApi.getDocument();
|
||||
this.group.zIndex(0);
|
||||
|
||||
const x = stage.x();
|
||||
const y = stage.y();
|
||||
const width = stage.width();
|
||||
const height = stage.height();
|
||||
const scale = stage.scaleX();
|
||||
const x = this.manager.stage.x();
|
||||
const y = this.manager.stage.y();
|
||||
const width = this.manager.stage.width();
|
||||
const height = this.manager.stage.height();
|
||||
const scale = this.manager.stage.scaleX();
|
||||
|
||||
this.outerRect.setAttrs({
|
||||
offsetX: x / scale,
|
||||
@ -52,16 +55,18 @@ export class CanvasDocumentSizeOverlay {
|
||||
});
|
||||
}
|
||||
|
||||
fitToStage(stage: Konva.Stage, document: CanvasV2State['document'], setStageAttrs: (attrs: StageAttrs) => void) {
|
||||
fitToStage() {
|
||||
const document = this.manager.stateApi.getDocument();
|
||||
|
||||
// Fit & center the document on the stage
|
||||
const width = stage.width();
|
||||
const height = stage.height();
|
||||
const width = this.manager.stage.width();
|
||||
const height = this.manager.stage.height();
|
||||
const docWidthWithBuffer = document.width + this.padding * 2;
|
||||
const docHeightWithBuffer = document.height + this.padding * 2;
|
||||
const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1);
|
||||
const x = (width - docWidthWithBuffer * scale) / 2 + this.padding * scale;
|
||||
const y = (height - docHeightWithBuffer * scale) / 2 + this.padding * scale;
|
||||
stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale });
|
||||
setStageAttrs({ x, y, width, height, scale });
|
||||
this.manager.stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale });
|
||||
this.manager.stateApi.setStageAttrs({ x, y, width, height, scale });
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ import {
|
||||
RASTER_LAYER_OBJECT_GROUP_NAME,
|
||||
RG_LAYER_OBJECT_GROUP_NAME,
|
||||
} from 'features/controlLayers/konva/naming';
|
||||
import { createBboxRect } from 'features/controlLayers/konva/renderers/objects';
|
||||
import { createBboxRect } from 'features/controlLayers/konva/objects';
|
||||
import { imageDataToDataURL } from 'features/controlLayers/konva/util';
|
||||
import type {
|
||||
BboxChangedArg,
|
@ -128,7 +128,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
||||
|
||||
//#region mouseenter
|
||||
stage.on('mouseenter', () => {
|
||||
manager.renderToolPreview();
|
||||
manager.preview.tool.render();
|
||||
});
|
||||
|
||||
//#region mousedown
|
||||
@ -249,7 +249,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
||||
setLastAddedPoint(pos);
|
||||
}
|
||||
}
|
||||
manager.renderToolPreview();
|
||||
manager.preview.tool.render();
|
||||
});
|
||||
|
||||
//#region mouseup
|
||||
@ -288,7 +288,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
||||
setLastMouseDownPos(null);
|
||||
}
|
||||
|
||||
manager.renderToolPreview();
|
||||
manager.preview.tool.render();
|
||||
});
|
||||
|
||||
//#region mousemove
|
||||
@ -394,7 +394,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
||||
}
|
||||
}
|
||||
}
|
||||
manager.renderToolPreview();
|
||||
manager.preview.tool.render();
|
||||
});
|
||||
|
||||
//#region mouseleave
|
||||
@ -423,7 +423,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
||||
}
|
||||
}
|
||||
|
||||
manager.renderToolPreview();
|
||||
manager.preview.tool.render();
|
||||
});
|
||||
|
||||
//#region wheel
|
||||
@ -464,11 +464,11 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
||||
stage.scaleY(newScale);
|
||||
stage.position(newPos);
|
||||
setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale });
|
||||
manager.renderBackground();
|
||||
manager.renderDocumentSizeOverlay();
|
||||
manager.preview.tool.render();
|
||||
manager.preview.documentSizeOverlay.render();
|
||||
}
|
||||
}
|
||||
manager.renderToolPreview();
|
||||
manager.preview.tool.render();
|
||||
});
|
||||
|
||||
//#region dragmove
|
||||
@ -480,9 +480,9 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
||||
height: stage.height(),
|
||||
scale: stage.scaleX(),
|
||||
});
|
||||
manager.renderBackground();
|
||||
manager.renderDocumentSizeOverlay();
|
||||
manager.renderToolPreview();
|
||||
manager.preview.tool.render();
|
||||
manager.preview.documentSizeOverlay.render();
|
||||
manager.preview.tool.render();
|
||||
});
|
||||
|
||||
//#region dragend
|
||||
@ -495,7 +495,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
||||
height: stage.height(),
|
||||
scale: stage.scaleX(),
|
||||
});
|
||||
manager.renderToolPreview();
|
||||
manager.preview.tool.render();
|
||||
});
|
||||
|
||||
//#region key
|
||||
@ -520,11 +520,12 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
||||
} else if (e.key === 'r') {
|
||||
setLastCursorPos(null);
|
||||
setLastMouseDownPos(null);
|
||||
manager.fitDocument();
|
||||
manager.renderBackground();
|
||||
manager.renderDocumentSizeOverlay();
|
||||
manager.preview.documentSizeOverlay.fitToStage();
|
||||
manager.preview.tool.render();
|
||||
|
||||
manager.preview.documentSizeOverlay.render();
|
||||
}
|
||||
manager.renderToolPreview();
|
||||
manager.preview.tool.render();
|
||||
};
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
|
||||
@ -542,7 +543,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
||||
setToolBuffer(null);
|
||||
setSpaceKey(false);
|
||||
}
|
||||
manager.renderToolPreview();
|
||||
manager.preview.tool.render();
|
||||
};
|
||||
window.addEventListener('keyup', onKeyUp);
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
||||
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
|
||||
import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox';
|
||||
import { getObjectGroupId,INPAINT_MASK_LAYER_ID } from 'features/controlLayers/konva/naming';
|
||||
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||
import { getNodeBboxFast } from 'features/controlLayers/konva/renderers/entityBbox';
|
||||
import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/renderers/objects';
|
||||
import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/objects';
|
||||
import { mapId } from 'features/controlLayers/konva/util';
|
||||
import { type InpaintMaskEntity, isDrawingTool } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
@ -19,10 +19,10 @@ export class CanvasInpaintMask {
|
||||
transformer: Konva.Transformer;
|
||||
objects: Map<string, KonvaBrushLine | KonvaEraserLine | KonvaRect>;
|
||||
|
||||
constructor(entity: InpaintMaskEntity, manager: KonvaNodeManager) {
|
||||
this.id = entity.id;
|
||||
constructor(manager: KonvaNodeManager) {
|
||||
this.id = INPAINT_MASK_LAYER_ID;
|
||||
this.manager = manager;
|
||||
this.layer = new Konva.Layer({ id: entity.id });
|
||||
this.layer = new Konva.Layer({ id: INPAINT_MASK_LAYER_ID });
|
||||
|
||||
this.group = new Konva.Group({
|
||||
id: getObjectGroupId(this.layer.id(), uuidv4()),
|
@ -1,6 +1,6 @@
|
||||
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
|
||||
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||
import { KonvaBrushLine, KonvaEraserLine, KonvaImage, KonvaRect } from 'features/controlLayers/konva/renderers/objects';
|
||||
import { KonvaBrushLine, KonvaEraserLine, KonvaImage, KonvaRect } from 'features/controlLayers/konva/objects';
|
||||
import { mapId } from 'features/controlLayers/konva/util';
|
||||
import { isDrawingTool, type LayerEntity } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
@ -39,11 +39,11 @@ export const RASTER_LAYER_ERASER_LINE_NAME = `${RASTER_LAYER_NAME}.eraser_line`;
|
||||
export const RASTER_LAYER_RECT_SHAPE_NAME = `${RASTER_LAYER_NAME}.rect_shape`;
|
||||
export const RASTER_LAYER_IMAGE_NAME = `${RASTER_LAYER_NAME}.image`;
|
||||
|
||||
export const INPAINT_MASK_LAYER_NAME = 'inpaint_mask_layer';
|
||||
export const INPAINT_MASK_LAYER_OBJECT_GROUP_NAME = `${INPAINT_MASK_LAYER_NAME}.object_group`;
|
||||
export const INPAINT_MASK_LAYER_BRUSH_LINE_NAME = `${INPAINT_MASK_LAYER_NAME}.brush_line`;
|
||||
export const INPAINT_MASK_LAYER_ERASER_LINE_NAME = `${INPAINT_MASK_LAYER_NAME}.eraser_line`;
|
||||
export const INPAINT_MASK_LAYER_RECT_SHAPE_NAME = `${INPAINT_MASK_LAYER_NAME}.rect_shape`;
|
||||
export const INPAINT_MASK_LAYER_ID = 'inpaint_mask_layer';
|
||||
export const INPAINT_MASK_LAYER_OBJECT_GROUP_NAME = `${INPAINT_MASK_LAYER_ID}.object_group`;
|
||||
export const INPAINT_MASK_LAYER_BRUSH_LINE_NAME = `${INPAINT_MASK_LAYER_ID}.brush_line`;
|
||||
export const INPAINT_MASK_LAYER_ERASER_LINE_NAME = `${INPAINT_MASK_LAYER_ID}.eraser_line`;
|
||||
export const INPAINT_MASK_LAYER_RECT_SHAPE_NAME = `${INPAINT_MASK_LAYER_ID}.rect_shape`;
|
||||
|
||||
export const BACKGROUND_LAYER_ID = 'background_layer';
|
||||
|
||||
|
@ -1,89 +1,28 @@
|
||||
import type { Store } from '@reduxjs/toolkit';
|
||||
import type { RootState } from 'app/store/store';
|
||||
import { getImageDataTransparency } from 'common/util/arrayBuffer';
|
||||
import { CanvasBackground } from 'features/controlLayers/konva/renderers/background';
|
||||
import { CanvasPreview } from 'features/controlLayers/konva/renderers/preview';
|
||||
import { CanvasBackground } from 'features/controlLayers/konva/background';
|
||||
import { setStageEventHandlers } from 'features/controlLayers/konva/events';
|
||||
import { CanvasPreview } from 'features/controlLayers/konva/preview';
|
||||
import { konvaNodeToBlob, konvaNodeToImageData, previewBlob } from 'features/controlLayers/konva/util';
|
||||
import type {
|
||||
BrushLineAddedArg,
|
||||
CanvasEntity,
|
||||
CanvasV2State,
|
||||
EraserLineAddedArg,
|
||||
GenerationMode,
|
||||
PointAddedToLineArg,
|
||||
PosChangedArg,
|
||||
Rect,
|
||||
RectShapeAddedArg,
|
||||
RgbaColor,
|
||||
ScaleChangedArg,
|
||||
StageAttrs,
|
||||
Tool,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import type { CanvasV2State, GenerationMode, Rect } from 'features/controlLayers/store/types';
|
||||
import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers';
|
||||
import type Konva from 'konva';
|
||||
import type { Vector2d } from 'konva/lib/types';
|
||||
import { atom } from 'nanostores';
|
||||
import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } from 'services/api/endpoints/images';
|
||||
import type { ImageCategory, ImageDTO } from 'services/api/types';
|
||||
import type { InvocationDenoiseProgressEvent } from 'services/events/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
import { CanvasBbox } from './renderers/bbox';
|
||||
import { CanvasControlAdapter } from './renderers/controlAdapters';
|
||||
import { CanvasDocumentSizeOverlay } from './renderers/documentSizeOverlay';
|
||||
import { CanvasInpaintMask } from './renderers/inpaintMask';
|
||||
import { CanvasLayer } from './renderers/layers';
|
||||
import { CanvasRegion } from './renderers/regions';
|
||||
import { CanvasStagingArea } from './renderers/stagingArea';
|
||||
import { CanvasTool } from './renderers/tool';
|
||||
|
||||
export type StateApi = {
|
||||
getToolState: () => CanvasV2State['tool'];
|
||||
getCurrentFill: () => RgbaColor;
|
||||
setTool: (tool: Tool) => void;
|
||||
setToolBuffer: (tool: Tool | null) => void;
|
||||
getIsDrawing: () => boolean;
|
||||
setIsDrawing: (isDrawing: boolean) => void;
|
||||
getIsMouseDown: () => boolean;
|
||||
setIsMouseDown: (isMouseDown: boolean) => void;
|
||||
getLastMouseDownPos: () => Vector2d | null;
|
||||
setLastMouseDownPos: (pos: Vector2d | null) => void;
|
||||
getLastCursorPos: () => Vector2d | null;
|
||||
setLastCursorPos: (pos: Vector2d | null) => void;
|
||||
getLastAddedPoint: () => Vector2d | null;
|
||||
setLastAddedPoint: (pos: Vector2d | null) => void;
|
||||
setStageAttrs: (attrs: StageAttrs) => void;
|
||||
getSelectedEntity: () => CanvasEntity | null;
|
||||
getSpaceKey: () => boolean;
|
||||
setSpaceKey: (val: boolean) => void;
|
||||
getShouldShowStagedImage: () => boolean;
|
||||
getBbox: () => CanvasV2State['bbox'];
|
||||
getSettings: () => CanvasV2State['settings'];
|
||||
onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void;
|
||||
onEraserLineAdded: (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => void;
|
||||
onPointAddedToLine: (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => void;
|
||||
onRectShapeAdded: (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => void;
|
||||
onBrushWidthChanged: (size: number) => void;
|
||||
onEraserWidthChanged: (size: number) => void;
|
||||
getMaskOpacity: () => number;
|
||||
getIsSelected: (id: string) => boolean;
|
||||
onScaleChanged: (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => void;
|
||||
onPosChanged: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void;
|
||||
onBboxTransformed: (bbox: Rect) => void;
|
||||
getShiftKey: () => boolean;
|
||||
getCtrlKey: () => boolean;
|
||||
getMetaKey: () => boolean;
|
||||
getAltKey: () => boolean;
|
||||
getDocument: () => CanvasV2State['document'];
|
||||
getLayersState: () => CanvasV2State['layers'];
|
||||
getControlAdaptersState: () => CanvasV2State['controlAdapters'];
|
||||
getRegionsState: () => CanvasV2State['regions'];
|
||||
getInpaintMaskState: () => CanvasV2State['inpaintMask'];
|
||||
getStagingAreaState: () => CanvasV2State['stagingArea'];
|
||||
getLastProgressEvent: () => InvocationDenoiseProgressEvent | null;
|
||||
resetLastProgressEvent: () => void;
|
||||
onInpaintMaskImageCached: (imageDTO: ImageDTO) => void;
|
||||
onRegionMaskImageCached: (id: string, imageDTO: ImageDTO) => void;
|
||||
onLayerImageCached: (imageDTO: ImageDTO) => void;
|
||||
};
|
||||
import { CanvasBbox } from './bbox';
|
||||
import { CanvasControlAdapter } from './controlAdapters';
|
||||
import { CanvasDocumentSizeOverlay } from './documentSizeOverlay';
|
||||
import { CanvasInpaintMask } from './inpaintMask';
|
||||
import { CanvasLayer } from './layers';
|
||||
import { CanvasRegion } from './regions';
|
||||
import { CanvasStagingArea } from './stagingArea';
|
||||
import { StateApi } from './stateApi';
|
||||
import { CanvasTool } from './tool';
|
||||
|
||||
type Util = {
|
||||
getImageDTO: (imageName: string) => Promise<ImageDTO | null>;
|
||||
@ -116,41 +55,44 @@ export class KonvaNodeManager {
|
||||
stateApi: StateApi;
|
||||
preview: CanvasPreview;
|
||||
background: CanvasBackground;
|
||||
private store: Store<RootState>;
|
||||
private isFirstRender: boolean;
|
||||
private prevState: CanvasV2State;
|
||||
private log: (message: string) => void;
|
||||
|
||||
constructor(
|
||||
stage: Konva.Stage,
|
||||
container: HTMLDivElement,
|
||||
stateApi: StateApi,
|
||||
store: Store<RootState>,
|
||||
log: (message: string) => void,
|
||||
getImageDTO: Util['getImageDTO'] = defaultGetImageDTO,
|
||||
uploadImage: Util['uploadImage'] = defaultUploadImage
|
||||
) {
|
||||
this.log = log;
|
||||
this.stage = stage;
|
||||
this.container = container;
|
||||
this.stateApi = stateApi;
|
||||
this.store = store;
|
||||
this.stateApi = new StateApi(this.store, this.log);
|
||||
this.prevState = this.stateApi.getState();
|
||||
this.isFirstRender = true;
|
||||
|
||||
this.util = {
|
||||
getImageDTO,
|
||||
uploadImage,
|
||||
};
|
||||
|
||||
this.preview = new CanvasPreview(
|
||||
new CanvasBbox(
|
||||
this.stateApi.getBbox,
|
||||
this.stateApi.onBboxTransformed,
|
||||
this.stateApi.getShiftKey,
|
||||
this.stateApi.getCtrlKey,
|
||||
this.stateApi.getMetaKey,
|
||||
this.stateApi.getAltKey
|
||||
),
|
||||
new CanvasTool(),
|
||||
new CanvasDocumentSizeOverlay(),
|
||||
new CanvasStagingArea()
|
||||
new CanvasBbox(this),
|
||||
new CanvasTool(this),
|
||||
new CanvasDocumentSizeOverlay(this),
|
||||
new CanvasStagingArea(this)
|
||||
);
|
||||
this.stage.add(this.preview.layer);
|
||||
|
||||
this.background = new CanvasBackground();
|
||||
this.background = new CanvasBackground(this);
|
||||
this.stage.add(this.background.layer);
|
||||
|
||||
this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this);
|
||||
this.inpaintMask = new CanvasInpaintMask(this);
|
||||
this.stage.add(this.inpaintMask.layer);
|
||||
|
||||
this.layers = new Map();
|
||||
@ -247,46 +189,6 @@ export class KonvaNodeManager {
|
||||
this.preview.layer.zIndex(++zIndex);
|
||||
}
|
||||
|
||||
renderDocumentSizeOverlay() {
|
||||
this.preview.documentSizeOverlay.render(this.stage, this.stateApi.getDocument());
|
||||
}
|
||||
|
||||
renderBbox() {
|
||||
this.preview.bbox.render(this.stateApi.getBbox(), this.stateApi.getToolState());
|
||||
}
|
||||
|
||||
renderToolPreview() {
|
||||
this.preview.tool.render(
|
||||
this.stage,
|
||||
1, // TODO(psyche): this should be renderable entity count
|
||||
this.stateApi.getToolState(),
|
||||
this.stateApi.getCurrentFill(),
|
||||
this.stateApi.getSelectedEntity(),
|
||||
this.stateApi.getLastCursorPos(),
|
||||
this.stateApi.getLastMouseDownPos(),
|
||||
this.stateApi.getIsDrawing(),
|
||||
this.stateApi.getIsMouseDown()
|
||||
);
|
||||
}
|
||||
|
||||
renderBackground() {
|
||||
this.background.renderBackground(this.stage);
|
||||
}
|
||||
|
||||
renderStagingArea() {
|
||||
this.preview.stagingArea.render(
|
||||
this.stateApi.getStagingAreaState(),
|
||||
this.stateApi.getBbox(),
|
||||
this.stateApi.getShouldShowStagedImage(),
|
||||
this.stateApi.getLastProgressEvent(),
|
||||
this.stateApi.resetLastProgressEvent
|
||||
);
|
||||
}
|
||||
|
||||
fitDocument() {
|
||||
this.preview.documentSizeOverlay.fitToStage(this.stage, this.stateApi.getDocument(), this.stateApi.setStageAttrs);
|
||||
}
|
||||
|
||||
fitStageToContainer() {
|
||||
this.stage.width(this.container.offsetWidth);
|
||||
this.stage.height(this.container.offsetHeight);
|
||||
@ -297,10 +199,150 @@ export class KonvaNodeManager {
|
||||
height: this.stage.height(),
|
||||
scale: this.stage.scaleX(),
|
||||
});
|
||||
this.renderBackground();
|
||||
this.renderDocumentSizeOverlay();
|
||||
this.background.renderBackground();
|
||||
this.preview.documentSizeOverlay.render();
|
||||
}
|
||||
|
||||
render = async () => {
|
||||
const state = this.stateApi.getState();
|
||||
|
||||
if (this.prevState === state && !this.isFirstRender) {
|
||||
this.log('No changes detected, skipping render');
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.isFirstRender ||
|
||||
state.layers.entities !== this.prevState.layers.entities ||
|
||||
state.tool.selected !== this.prevState.tool.selected ||
|
||||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||
) {
|
||||
this.log('Rendering layers');
|
||||
this.renderLayers();
|
||||
}
|
||||
|
||||
if (
|
||||
this.isFirstRender ||
|
||||
state.regions.entities !== this.prevState.regions.entities ||
|
||||
state.settings.maskOpacity !== this.prevState.settings.maskOpacity ||
|
||||
state.tool.selected !== this.prevState.tool.selected ||
|
||||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||
) {
|
||||
this.log('Rendering regions');
|
||||
this.renderRegions();
|
||||
}
|
||||
|
||||
if (
|
||||
this.isFirstRender ||
|
||||
state.inpaintMask !== this.prevState.inpaintMask ||
|
||||
state.settings.maskOpacity !== this.prevState.settings.maskOpacity ||
|
||||
state.tool.selected !== this.prevState.tool.selected ||
|
||||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||
) {
|
||||
this.log('Rendering inpaint mask');
|
||||
this.renderInpaintMask();
|
||||
}
|
||||
|
||||
if (
|
||||
this.isFirstRender ||
|
||||
state.controlAdapters.entities !== this.prevState.controlAdapters.entities ||
|
||||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||
) {
|
||||
this.log('Rendering control adapters');
|
||||
this.renderControlAdapters();
|
||||
}
|
||||
|
||||
if (this.isFirstRender || state.document !== this.prevState.document) {
|
||||
this.log('Rendering document bounds overlay');
|
||||
this.preview.documentSizeOverlay.render();
|
||||
}
|
||||
|
||||
if (
|
||||
this.isFirstRender ||
|
||||
state.bbox !== this.prevState.bbox ||
|
||||
state.tool.selected !== this.prevState.tool.selected
|
||||
) {
|
||||
this.log('Rendering generation bbox');
|
||||
this.preview.bbox.render();
|
||||
}
|
||||
|
||||
if (
|
||||
this.isFirstRender ||
|
||||
state.layers !== this.prevState.layers ||
|
||||
state.controlAdapters !== this.prevState.controlAdapters ||
|
||||
state.regions !== this.prevState.regions
|
||||
) {
|
||||
// this.log('Updating entity bboxes');
|
||||
// debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged);
|
||||
}
|
||||
|
||||
if (this.isFirstRender || state.stagingArea !== this.prevState.stagingArea) {
|
||||
this.log('Rendering staging area');
|
||||
this.preview.stagingArea.render();
|
||||
}
|
||||
|
||||
if (
|
||||
this.isFirstRender ||
|
||||
state.layers.entities !== this.prevState.layers.entities ||
|
||||
state.controlAdapters.entities !== this.prevState.controlAdapters.entities ||
|
||||
state.regions.entities !== this.prevState.regions.entities ||
|
||||
state.inpaintMask !== this.prevState.inpaintMask ||
|
||||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||
) {
|
||||
this.log('Arranging entities');
|
||||
this.arrangeEntities();
|
||||
}
|
||||
|
||||
this.prevState = state;
|
||||
|
||||
if (this.isFirstRender) {
|
||||
this.isFirstRender = false;
|
||||
}
|
||||
};
|
||||
|
||||
initialize = () => {
|
||||
this.log('Initializing renderer');
|
||||
this.stage.container(this.container);
|
||||
|
||||
const cleanupListeners = setStageEventHandlers(this);
|
||||
|
||||
// We can use a resize observer to ensure the stage always fits the container. We also need to re-render the bg and
|
||||
// document bounds overlay when the stage is resized.
|
||||
const resizeObserver = new ResizeObserver(this.fitStageToContainer.bind(this));
|
||||
resizeObserver.observe(this.container);
|
||||
this.fitStageToContainer();
|
||||
|
||||
const unsubscribeRenderer = this.store.subscribe(this.render);
|
||||
|
||||
// When we this flag, we need to render the staging area
|
||||
$shouldShowStagedImage.subscribe((shouldShowStagedImage, prevShouldShowStagedImage) => {
|
||||
this.log('Rendering staging area');
|
||||
if (shouldShowStagedImage !== prevShouldShowStagedImage) {
|
||||
this.preview.stagingArea.render();
|
||||
}
|
||||
});
|
||||
|
||||
$lastProgressEvent.subscribe(() => {
|
||||
this.log('Rendering staging area');
|
||||
this.preview.stagingArea.render();
|
||||
});
|
||||
|
||||
this.log('First render of konva stage');
|
||||
// On first render, the document should be fit to the stage.
|
||||
this.preview.documentSizeOverlay.render();
|
||||
this.preview.documentSizeOverlay.fitToStage();
|
||||
this.preview.tool.render();
|
||||
this.render();
|
||||
|
||||
return () => {
|
||||
this.log('Cleaning up konva renderer');
|
||||
unsubscribeRenderer();
|
||||
cleanupListeners();
|
||||
$shouldShowStagedImage.off();
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
};
|
||||
|
||||
getInpaintMaskLayerClone(): Konva.Layer {
|
||||
const layerClone = this.inpaintMask.layer.clone();
|
||||
const objectGroupClone = this.inpaintMask.group.clone();
|
||||
|
@ -1,13 +1,10 @@
|
||||
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
||||
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
|
||||
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||
import { getNodeBboxFast } from 'features/controlLayers/konva/renderers/entityBbox';
|
||||
import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/renderers/objects';
|
||||
import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox';
|
||||
import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/objects';
|
||||
import { mapId } from 'features/controlLayers/konva/util';
|
||||
import {
|
||||
isDrawingTool,
|
||||
type RegionEntity,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { isDrawingTool, type RegionEntity } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import { assert } from 'tsafe';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
@ -1,499 +0,0 @@
|
||||
import { $alt, $ctrl, $meta, $shift } from '@invoke-ai/ui-library';
|
||||
import type { Store } from '@reduxjs/toolkit';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { $isDebugging } from 'app/store/nanostores/isDebugging';
|
||||
import type { RootState } from 'app/store/store';
|
||||
import { setStageEventHandlers } from 'features/controlLayers/konva/events';
|
||||
import { KonvaNodeManager, setNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||
import { updateBboxes } from 'features/controlLayers/konva/renderers/entityBbox';
|
||||
import {
|
||||
$lastProgressEvent,
|
||||
$shouldShowStagedImage,
|
||||
$stageAttrs,
|
||||
bboxChanged,
|
||||
brushWidthChanged,
|
||||
caBboxChanged,
|
||||
caTranslated,
|
||||
eraserWidthChanged,
|
||||
imBboxChanged,
|
||||
imBrushLineAdded,
|
||||
imEraserLineAdded,
|
||||
imImageCacheChanged,
|
||||
imLinePointAdded,
|
||||
imRectAdded,
|
||||
imScaled,
|
||||
imTranslated,
|
||||
layerBboxChanged,
|
||||
layerBrushLineAdded,
|
||||
layerEraserLineAdded,
|
||||
layerImageCacheChanged,
|
||||
layerLinePointAdded,
|
||||
layerRectAdded,
|
||||
layerScaled,
|
||||
layerTranslated,
|
||||
rgBboxChanged,
|
||||
rgBrushLineAdded,
|
||||
rgEraserLineAdded,
|
||||
rgImageCacheChanged,
|
||||
rgLinePointAdded,
|
||||
rgRectAdded,
|
||||
rgScaled,
|
||||
rgTranslated,
|
||||
toolBufferChanged,
|
||||
toolChanged,
|
||||
} from 'features/controlLayers/store/canvasV2Slice';
|
||||
import type {
|
||||
BboxChangedArg,
|
||||
BrushLineAddedArg,
|
||||
CanvasEntity,
|
||||
CanvasV2State,
|
||||
EraserLineAddedArg,
|
||||
PointAddedToLineArg,
|
||||
PosChangedArg,
|
||||
RectShapeAddedArg,
|
||||
ScaleChangedArg,
|
||||
Tool,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import type Konva from 'konva';
|
||||
import type { IRect, Vector2d } from 'konva/lib/types';
|
||||
import { debounce } from 'lodash-es';
|
||||
import type { RgbaColor } from 'react-colorful';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
/**
|
||||
* Initializes the canvas renderer. It subscribes to the redux store and listens for changes directly, bypassing the
|
||||
* react rendering cycle entirely, improving canvas performance.
|
||||
* @param store The redux store
|
||||
* @param stage The konva stage
|
||||
* @param container The stage's target container element
|
||||
* @returns A cleanup function
|
||||
*/
|
||||
export const initializeRenderer = (
|
||||
store: Store<RootState>,
|
||||
stage: Konva.Stage,
|
||||
container: HTMLDivElement | null
|
||||
): (() => void) => {
|
||||
const _log = logger('konva');
|
||||
/**
|
||||
* Logs a message to the console if debugging is enabled.
|
||||
*/
|
||||
const logIfDebugging = (message: string) => {
|
||||
if ($isDebugging.get()) {
|
||||
_log.debug(message);
|
||||
}
|
||||
};
|
||||
|
||||
logIfDebugging('Initializing renderer');
|
||||
if (!container) {
|
||||
// Nothing to clean up
|
||||
logIfDebugging('No stage container, skipping initialization');
|
||||
return () => {};
|
||||
}
|
||||
|
||||
stage.container(container);
|
||||
|
||||
// Set up callbacks for various events
|
||||
const onPosChanged = (arg: PosChangedArg, entityType: CanvasEntity['type']) => {
|
||||
logIfDebugging('onPosChanged');
|
||||
if (entityType === 'layer') {
|
||||
dispatch(layerTranslated(arg));
|
||||
} else if (entityType === 'control_adapter') {
|
||||
dispatch(caTranslated(arg));
|
||||
} else if (entityType === 'regional_guidance') {
|
||||
dispatch(rgTranslated(arg));
|
||||
} else if (entityType === 'inpaint_mask') {
|
||||
dispatch(imTranslated(arg));
|
||||
}
|
||||
};
|
||||
const onScaleChanged = (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => {
|
||||
logIfDebugging('onScaleChanged');
|
||||
if (entityType === 'layer') {
|
||||
dispatch(layerScaled(arg));
|
||||
} else if (entityType === 'inpaint_mask') {
|
||||
dispatch(imScaled(arg));
|
||||
} else if (entityType === 'regional_guidance') {
|
||||
dispatch(rgScaled(arg));
|
||||
}
|
||||
};
|
||||
const onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => {
|
||||
logIfDebugging('Entity bbox changed');
|
||||
if (entityType === 'layer') {
|
||||
dispatch(layerBboxChanged(arg));
|
||||
} else if (entityType === 'control_adapter') {
|
||||
dispatch(caBboxChanged(arg));
|
||||
} else if (entityType === 'regional_guidance') {
|
||||
dispatch(rgBboxChanged(arg));
|
||||
} else if (entityType === 'inpaint_mask') {
|
||||
dispatch(imBboxChanged(arg));
|
||||
}
|
||||
};
|
||||
const onBrushLineAdded = (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => {
|
||||
logIfDebugging('Brush line added');
|
||||
if (entityType === 'layer') {
|
||||
dispatch(layerBrushLineAdded(arg));
|
||||
} else if (entityType === 'regional_guidance') {
|
||||
dispatch(rgBrushLineAdded(arg));
|
||||
} else if (entityType === 'inpaint_mask') {
|
||||
dispatch(imBrushLineAdded(arg));
|
||||
}
|
||||
};
|
||||
const onEraserLineAdded = (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => {
|
||||
logIfDebugging('Eraser line added');
|
||||
if (entityType === 'layer') {
|
||||
dispatch(layerEraserLineAdded(arg));
|
||||
} else if (entityType === 'regional_guidance') {
|
||||
dispatch(rgEraserLineAdded(arg));
|
||||
} else if (entityType === 'inpaint_mask') {
|
||||
dispatch(imEraserLineAdded(arg));
|
||||
}
|
||||
};
|
||||
const onPointAddedToLine = (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => {
|
||||
logIfDebugging('Point added to line');
|
||||
if (entityType === 'layer') {
|
||||
dispatch(layerLinePointAdded(arg));
|
||||
} else if (entityType === 'regional_guidance') {
|
||||
dispatch(rgLinePointAdded(arg));
|
||||
} else if (entityType === 'inpaint_mask') {
|
||||
dispatch(imLinePointAdded(arg));
|
||||
}
|
||||
};
|
||||
const onRectShapeAdded = (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => {
|
||||
logIfDebugging('Rect shape added');
|
||||
if (entityType === 'layer') {
|
||||
dispatch(layerRectAdded(arg));
|
||||
} else if (entityType === 'regional_guidance') {
|
||||
dispatch(rgRectAdded(arg));
|
||||
} else if (entityType === 'inpaint_mask') {
|
||||
dispatch(imRectAdded(arg));
|
||||
}
|
||||
};
|
||||
const onBboxTransformed = (bbox: IRect) => {
|
||||
logIfDebugging('Generation bbox transformed');
|
||||
dispatch(bboxChanged(bbox));
|
||||
};
|
||||
const onBrushWidthChanged = (width: number) => {
|
||||
logIfDebugging('Brush width changed');
|
||||
dispatch(brushWidthChanged(width));
|
||||
};
|
||||
const onEraserWidthChanged = (width: number) => {
|
||||
logIfDebugging('Eraser width changed');
|
||||
dispatch(eraserWidthChanged(width));
|
||||
};
|
||||
const onRegionMaskImageCached = (id: string, imageDTO: ImageDTO) => {
|
||||
logIfDebugging('Region mask image cached');
|
||||
dispatch(rgImageCacheChanged({ id, imageDTO }));
|
||||
};
|
||||
const onInpaintMaskImageCached = (imageDTO: ImageDTO) => {
|
||||
logIfDebugging('Inpaint mask image cached');
|
||||
dispatch(imImageCacheChanged({ imageDTO }));
|
||||
};
|
||||
const onLayerImageCached = (imageDTO: ImageDTO) => {
|
||||
logIfDebugging('Layer image cached');
|
||||
dispatch(layerImageCacheChanged({ imageDTO }));
|
||||
};
|
||||
|
||||
const setTool = (tool: Tool) => {
|
||||
logIfDebugging('Tool selection changed');
|
||||
dispatch(toolChanged(tool));
|
||||
};
|
||||
const setToolBuffer = (toolBuffer: Tool | null) => {
|
||||
logIfDebugging('Tool buffer changed');
|
||||
dispatch(toolBufferChanged(toolBuffer));
|
||||
};
|
||||
|
||||
const selectSelectedEntity = (canvasV2: CanvasV2State): CanvasEntity | null => {
|
||||
const identifier = canvasV2.selectedEntityIdentifier;
|
||||
let selectedEntity: CanvasEntity | null = null;
|
||||
if (!identifier) {
|
||||
selectedEntity = null;
|
||||
} else if (identifier.type === 'layer') {
|
||||
selectedEntity = canvasV2.layers.entities.find((i) => i.id === identifier.id) ?? null;
|
||||
} else if (identifier.type === 'control_adapter') {
|
||||
selectedEntity = canvasV2.controlAdapters.entities.find((i) => i.id === identifier.id) ?? null;
|
||||
} else if (identifier.type === 'ip_adapter') {
|
||||
selectedEntity = canvasV2.ipAdapters.entities.find((i) => i.id === identifier.id) ?? null;
|
||||
} else if (identifier.type === 'regional_guidance') {
|
||||
selectedEntity = canvasV2.regions.entities.find((i) => i.id === identifier.id) ?? null;
|
||||
} else if (identifier.type === 'inpaint_mask') {
|
||||
selectedEntity = canvasV2.inpaintMask;
|
||||
} else {
|
||||
selectedEntity = null;
|
||||
}
|
||||
return selectedEntity;
|
||||
};
|
||||
|
||||
const selectCurrentFill = (canvasV2: CanvasV2State, selectedEntity: CanvasEntity | null) => {
|
||||
let currentFill: RgbaColor = canvasV2.tool.fill;
|
||||
if (selectedEntity) {
|
||||
if (selectedEntity.type === 'regional_guidance') {
|
||||
currentFill = { ...selectedEntity.fill, a: canvasV2.settings.maskOpacity };
|
||||
} else if (selectedEntity.type === 'inpaint_mask') {
|
||||
currentFill = { ...canvasV2.inpaintMask.fill, a: canvasV2.settings.maskOpacity };
|
||||
}
|
||||
} else {
|
||||
currentFill = canvasV2.tool.fill;
|
||||
}
|
||||
return currentFill;
|
||||
};
|
||||
|
||||
const { getState, subscribe, dispatch } = store;
|
||||
|
||||
// On the first render, we need to render everything.
|
||||
let isFirstRender = true;
|
||||
|
||||
// Stage interaction listeners need helpers to get and update current state. Some of the state is read-only, like
|
||||
// bbox, document and tool state, while interaction state is read-write.
|
||||
|
||||
// Read-only state, derived from redux
|
||||
let prevCanvasV2 = getState().canvasV2;
|
||||
let canvasV2 = getState().canvasV2;
|
||||
const getSelectedEntity = () => selectSelectedEntity(canvasV2);
|
||||
const getCurrentFill = () => selectCurrentFill(canvasV2, getSelectedEntity());
|
||||
const getBbox = () => canvasV2.bbox;
|
||||
const getDocument = () => canvasV2.document;
|
||||
const getToolState = () => canvasV2.tool;
|
||||
const getSettings = () => canvasV2.settings;
|
||||
const getRegionsState = () => canvasV2.regions;
|
||||
const getLayersState = () => canvasV2.layers;
|
||||
const getControlAdaptersState = () => canvasV2.controlAdapters;
|
||||
const getInpaintMaskState = () => canvasV2.inpaintMask;
|
||||
const getMaskOpacity = () => canvasV2.settings.maskOpacity;
|
||||
const getStagingAreaState = () => canvasV2.stagingArea;
|
||||
const getIsSelected = (id: string) => getSelectedEntity()?.id === id;
|
||||
|
||||
// Read-only state, derived from nanostores
|
||||
const resetLastProgressEvent = () => {
|
||||
$lastProgressEvent.set(null);
|
||||
};
|
||||
// Read-write state, ephemeral interaction state
|
||||
let isDrawing = false;
|
||||
const getIsDrawing = () => isDrawing;
|
||||
const setIsDrawing = (val: boolean) => {
|
||||
isDrawing = val;
|
||||
};
|
||||
|
||||
let isMouseDown = false;
|
||||
const getIsMouseDown = () => isMouseDown;
|
||||
const setIsMouseDown = (val: boolean) => {
|
||||
isMouseDown = val;
|
||||
};
|
||||
|
||||
let lastAddedPoint: Vector2d | null = null;
|
||||
const getLastAddedPoint = () => lastAddedPoint;
|
||||
const setLastAddedPoint = (val: Vector2d | null) => {
|
||||
lastAddedPoint = val;
|
||||
};
|
||||
|
||||
let lastMouseDownPos: Vector2d | null = null;
|
||||
const getLastMouseDownPos = () => lastMouseDownPos;
|
||||
const setLastMouseDownPos = (val: Vector2d | null) => {
|
||||
lastMouseDownPos = val;
|
||||
};
|
||||
|
||||
let lastCursorPos: Vector2d | null = null;
|
||||
const getLastCursorPos = () => lastCursorPos;
|
||||
const setLastCursorPos = (val: Vector2d | null) => {
|
||||
lastCursorPos = val;
|
||||
};
|
||||
|
||||
let spaceKey = false;
|
||||
const getSpaceKey = () => spaceKey;
|
||||
const setSpaceKey = (val: boolean) => {
|
||||
spaceKey = val;
|
||||
};
|
||||
|
||||
const stateApi: KonvaNodeManager['stateApi'] = {
|
||||
// Read-only state
|
||||
getToolState,
|
||||
getSelectedEntity,
|
||||
getBbox,
|
||||
getSettings,
|
||||
getCurrentFill,
|
||||
getAltKey: $alt.get,
|
||||
getCtrlKey: $ctrl.get,
|
||||
getMetaKey: $meta.get,
|
||||
getShiftKey: $shift.get,
|
||||
getControlAdaptersState,
|
||||
getDocument,
|
||||
getLayersState,
|
||||
getRegionsState,
|
||||
getMaskOpacity,
|
||||
getInpaintMaskState,
|
||||
getStagingAreaState,
|
||||
getShouldShowStagedImage: $shouldShowStagedImage.get,
|
||||
getLastProgressEvent: $lastProgressEvent.get,
|
||||
resetLastProgressEvent,
|
||||
getIsSelected,
|
||||
|
||||
// Read-write state
|
||||
setTool,
|
||||
setToolBuffer,
|
||||
getIsDrawing,
|
||||
setIsDrawing,
|
||||
getIsMouseDown,
|
||||
setIsMouseDown,
|
||||
getLastAddedPoint,
|
||||
setLastAddedPoint,
|
||||
getLastCursorPos,
|
||||
setLastCursorPos,
|
||||
getLastMouseDownPos,
|
||||
setLastMouseDownPos,
|
||||
getSpaceKey,
|
||||
setSpaceKey,
|
||||
setStageAttrs: $stageAttrs.set,
|
||||
|
||||
// Callbacks
|
||||
onBrushLineAdded,
|
||||
onEraserLineAdded,
|
||||
onPointAddedToLine,
|
||||
onRectShapeAdded,
|
||||
onBrushWidthChanged,
|
||||
onEraserWidthChanged,
|
||||
onPosChanged,
|
||||
onBboxTransformed,
|
||||
onRegionMaskImageCached,
|
||||
onInpaintMaskImageCached,
|
||||
onLayerImageCached,
|
||||
onScaleChanged,
|
||||
};
|
||||
|
||||
const manager = new KonvaNodeManager(stage, container, stateApi);
|
||||
setNodeManager(manager);
|
||||
console.log(manager);
|
||||
|
||||
const cleanupListeners = setStageEventHandlers(manager);
|
||||
|
||||
// Calculating bounding boxes is expensive, must be debounced to not block the UI thread during a user interaction.
|
||||
// TODO(psyche): Figure out how to do this in a worker. Probably means running the renderer in a worker and sending
|
||||
// the entire state over when needed.
|
||||
const debouncedUpdateBboxes = debounce(updateBboxes, 300);
|
||||
|
||||
const renderCanvas = async () => {
|
||||
canvasV2 = store.getState().canvasV2;
|
||||
|
||||
if (prevCanvasV2 === canvasV2 && !isFirstRender) {
|
||||
logIfDebugging('No changes detected, skipping render');
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isFirstRender ||
|
||||
canvasV2.layers.entities !== prevCanvasV2.layers.entities ||
|
||||
canvasV2.tool.selected !== prevCanvasV2.tool.selected ||
|
||||
canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id
|
||||
) {
|
||||
logIfDebugging('Rendering layers');
|
||||
manager.renderLayers();
|
||||
}
|
||||
|
||||
if (
|
||||
isFirstRender ||
|
||||
canvasV2.regions.entities !== prevCanvasV2.regions.entities ||
|
||||
canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity ||
|
||||
canvasV2.tool.selected !== prevCanvasV2.tool.selected ||
|
||||
canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id
|
||||
) {
|
||||
logIfDebugging('Rendering regions');
|
||||
manager.renderRegions();
|
||||
}
|
||||
|
||||
if (
|
||||
isFirstRender ||
|
||||
canvasV2.inpaintMask !== prevCanvasV2.inpaintMask ||
|
||||
canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity ||
|
||||
canvasV2.tool.selected !== prevCanvasV2.tool.selected ||
|
||||
canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id
|
||||
) {
|
||||
logIfDebugging('Rendering inpaint mask');
|
||||
manager.renderInpaintMask();
|
||||
}
|
||||
|
||||
if (
|
||||
isFirstRender ||
|
||||
canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities ||
|
||||
canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id
|
||||
) {
|
||||
logIfDebugging('Rendering control adapters');
|
||||
manager.renderControlAdapters();
|
||||
}
|
||||
|
||||
if (isFirstRender || canvasV2.document !== prevCanvasV2.document) {
|
||||
logIfDebugging('Rendering document bounds overlay');
|
||||
manager.renderDocumentSizeOverlay();
|
||||
}
|
||||
|
||||
if (isFirstRender || canvasV2.bbox !== prevCanvasV2.bbox || canvasV2.tool.selected !== prevCanvasV2.tool.selected) {
|
||||
logIfDebugging('Rendering generation bbox');
|
||||
manager.renderBbox();
|
||||
}
|
||||
|
||||
if (
|
||||
isFirstRender ||
|
||||
canvasV2.layers !== prevCanvasV2.layers ||
|
||||
canvasV2.controlAdapters !== prevCanvasV2.controlAdapters ||
|
||||
canvasV2.regions !== prevCanvasV2.regions
|
||||
) {
|
||||
// logIfDebugging('Updating entity bboxes');
|
||||
// debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged);
|
||||
}
|
||||
|
||||
if (isFirstRender || canvasV2.stagingArea !== prevCanvasV2.stagingArea) {
|
||||
logIfDebugging('Rendering staging area');
|
||||
manager.renderStagingArea();
|
||||
}
|
||||
|
||||
if (
|
||||
isFirstRender ||
|
||||
canvasV2.layers.entities !== prevCanvasV2.layers.entities ||
|
||||
canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities ||
|
||||
canvasV2.regions.entities !== prevCanvasV2.regions.entities ||
|
||||
canvasV2.inpaintMask !== prevCanvasV2.inpaintMask ||
|
||||
canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id
|
||||
) {
|
||||
logIfDebugging('Arranging entities');
|
||||
manager.arrangeEntities();
|
||||
}
|
||||
|
||||
prevCanvasV2 = canvasV2;
|
||||
|
||||
if (isFirstRender) {
|
||||
isFirstRender = false;
|
||||
}
|
||||
};
|
||||
|
||||
// We can use a resize observer to ensure the stage always fits the container. We also need to re-render the bg and
|
||||
// document bounds overlay when the stage is resized.
|
||||
const resizeObserver = new ResizeObserver(manager.fitStageToContainer.bind(manager));
|
||||
resizeObserver.observe(container);
|
||||
manager.fitStageToContainer();
|
||||
|
||||
const unsubscribeRenderer = subscribe(renderCanvas);
|
||||
|
||||
// When we this flag, we need to render the staging area
|
||||
$shouldShowStagedImage.subscribe((shouldShowStagedImage, prevShouldShowStagedImage) => {
|
||||
logIfDebugging('Rendering staging area');
|
||||
if (shouldShowStagedImage !== prevShouldShowStagedImage) {
|
||||
manager.renderStagingArea();
|
||||
}
|
||||
});
|
||||
|
||||
$lastProgressEvent.subscribe(() => {
|
||||
logIfDebugging('Rendering staging area');
|
||||
manager.renderStagingArea();
|
||||
});
|
||||
|
||||
logIfDebugging('First render of konva stage');
|
||||
// On first render, the document should be fit to the stage.
|
||||
manager.renderDocumentSizeOverlay();
|
||||
manager.fitDocument();
|
||||
manager.renderToolPreview();
|
||||
renderCanvas();
|
||||
|
||||
return () => {
|
||||
logIfDebugging('Cleaning up konva renderer');
|
||||
unsubscribeRenderer();
|
||||
cleanupListeners();
|
||||
$shouldShowStagedImage.off();
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
};
|
@ -1,29 +1,29 @@
|
||||
import { KonvaImage, KonvaProgressImage } from 'features/controlLayers/konva/renderers/objects';
|
||||
import type { CanvasV2State } from 'features/controlLayers/store/types';
|
||||
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||
import { KonvaImage, KonvaProgressImage } from 'features/controlLayers/konva/objects';
|
||||
import Konva from 'konva';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import type { InvocationDenoiseProgressEvent } from 'services/events/types';
|
||||
|
||||
export class CanvasStagingArea {
|
||||
group: Konva.Group;
|
||||
image: KonvaImage | null;
|
||||
progressImage: KonvaProgressImage | null;
|
||||
imageDTO: ImageDTO | null;
|
||||
manager: KonvaNodeManager;
|
||||
|
||||
constructor() {
|
||||
constructor(manager: KonvaNodeManager) {
|
||||
this.manager = manager;
|
||||
this.group = new Konva.Group({ listening: false });
|
||||
this.image = null;
|
||||
this.progressImage = null;
|
||||
this.imageDTO = null;
|
||||
}
|
||||
|
||||
async render(
|
||||
stagingArea: CanvasV2State['stagingArea'],
|
||||
bbox: CanvasV2State['bbox'],
|
||||
shouldShowStagedImage: boolean,
|
||||
lastProgressEvent: InvocationDenoiseProgressEvent | null,
|
||||
resetLastProgressEvent: () => void
|
||||
) {
|
||||
async render() {
|
||||
const stagingArea = this.manager.stateApi.getStagingAreaState();
|
||||
const bbox = this.manager.stateApi.getBbox();
|
||||
const shouldShowStagedImage = this.manager.stateApi.getShouldShowStagedImage();
|
||||
const lastProgressEvent = this.manager.stateApi.getLastProgressEvent();
|
||||
|
||||
this.imageDTO = stagingArea.images[stagingArea.selectedImageIndex] ?? null;
|
||||
|
||||
if (this.imageDTO) {
|
||||
@ -58,7 +58,7 @@ export class CanvasStagingArea {
|
||||
konvaImage.width(this.imageDTO.width);
|
||||
konvaImage.height(this.imageDTO.height);
|
||||
}
|
||||
resetLastProgressEvent();
|
||||
this.manager.stateApi.resetLastProgressEvent();
|
||||
},
|
||||
}
|
||||
);
|
||||
@ -100,7 +100,7 @@ export class CanvasStagingArea {
|
||||
if (this.progressImage) {
|
||||
this.progressImage.konvaImageGroup.visible(false);
|
||||
}
|
||||
resetLastProgressEvent();
|
||||
this.manager.stateApi.resetLastProgressEvent();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,237 @@
|
||||
import { $alt, $ctrl, $meta, $shift } from "@invoke-ai/ui-library";
|
||||
import type { Store } from "@reduxjs/toolkit";
|
||||
import type { RootState } from "app/store/store";
|
||||
import { $isDrawing, $isMouseDown, $lastAddedPoint, $lastCursorPos, $lastMouseDownPos, $lastProgressEvent, $shouldShowStagedImage, $spaceKey, $stageAttrs, bboxChanged, brushWidthChanged, caBboxChanged, caTranslated, eraserWidthChanged, imBboxChanged, imBrushLineAdded, imEraserLineAdded, imImageCacheChanged, imLinePointAdded, imRectAdded, imScaled, imTranslated, layerBboxChanged, layerBrushLineAdded, layerEraserLineAdded, layerImageCacheChanged, layerLinePointAdded, layerRectAdded, layerScaled, layerTranslated, rgBboxChanged, rgBrushLineAdded, rgEraserLineAdded, rgImageCacheChanged, rgLinePointAdded, rgRectAdded, rgScaled, rgTranslated, toolBufferChanged, toolChanged } from "features/controlLayers/store/canvasV2Slice";
|
||||
import type { BboxChangedArg, BrushLineAddedArg, CanvasEntity, EraserLineAddedArg, PointAddedToLineArg, PosChangedArg, RectShapeAddedArg, ScaleChangedArg, Tool } from "features/controlLayers/store/types";
|
||||
import type { IRect } from "konva/lib/types";
|
||||
import type { RgbaColor } from "react-colorful";
|
||||
import type { ImageDTO } from "services/api/types";
|
||||
|
||||
|
||||
export class StateApi {
|
||||
private store: Store<RootState>;
|
||||
private log: (message: string) => void;
|
||||
|
||||
constructor(store: Store<RootState>, log: (message: string) => void) {
|
||||
this.store = store;
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
// Reminder - use arrow functions to avoid binding issues
|
||||
getState = () => {
|
||||
return this.store.getState().canvasV2;
|
||||
};
|
||||
|
||||
onPosChanged = (arg: PosChangedArg, entityType: CanvasEntity['type']) => {
|
||||
this.log('onPosChanged');
|
||||
if (entityType === 'layer') {
|
||||
this.store.dispatch(layerTranslated(arg));
|
||||
} else if (entityType === 'control_adapter') {
|
||||
this.store.dispatch(caTranslated(arg));
|
||||
} else if (entityType === 'regional_guidance') {
|
||||
this.store.dispatch(rgTranslated(arg));
|
||||
} else if (entityType === 'inpaint_mask') {
|
||||
this.store.dispatch(imTranslated(arg));
|
||||
}
|
||||
};
|
||||
onScaleChanged = (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => {
|
||||
this.log('onScaleChanged');
|
||||
if (entityType === 'layer') {
|
||||
this.store.dispatch(layerScaled(arg));
|
||||
} else if (entityType === 'inpaint_mask') {
|
||||
this.store.dispatch(imScaled(arg));
|
||||
} else if (entityType === 'regional_guidance') {
|
||||
this.store.dispatch(rgScaled(arg));
|
||||
}
|
||||
};
|
||||
onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => {
|
||||
this.log('Entity bbox changed');
|
||||
if (entityType === 'layer') {
|
||||
this.store.dispatch(layerBboxChanged(arg));
|
||||
} else if (entityType === 'control_adapter') {
|
||||
this.store.dispatch(caBboxChanged(arg));
|
||||
} else if (entityType === 'regional_guidance') {
|
||||
this.store.dispatch(rgBboxChanged(arg));
|
||||
} else if (entityType === 'inpaint_mask') {
|
||||
this.store.dispatch(imBboxChanged(arg));
|
||||
}
|
||||
};
|
||||
onBrushLineAdded = (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => {
|
||||
this.log('Brush line added');
|
||||
if (entityType === 'layer') {
|
||||
this.store.dispatch(layerBrushLineAdded(arg));
|
||||
} else if (entityType === 'regional_guidance') {
|
||||
this.store.dispatch(rgBrushLineAdded(arg));
|
||||
} else if (entityType === 'inpaint_mask') {
|
||||
this.store.dispatch(imBrushLineAdded(arg));
|
||||
}
|
||||
};
|
||||
onEraserLineAdded = (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => {
|
||||
this.log('Eraser line added');
|
||||
if (entityType === 'layer') {
|
||||
this.store.dispatch(layerEraserLineAdded(arg));
|
||||
} else if (entityType === 'regional_guidance') {
|
||||
this.store.dispatch(rgEraserLineAdded(arg));
|
||||
} else if (entityType === 'inpaint_mask') {
|
||||
this.store.dispatch(imEraserLineAdded(arg));
|
||||
}
|
||||
};
|
||||
onPointAddedToLine = (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => {
|
||||
this.log('Point added to line');
|
||||
if (entityType === 'layer') {
|
||||
this.store.dispatch(layerLinePointAdded(arg));
|
||||
} else if (entityType === 'regional_guidance') {
|
||||
this.store.dispatch(rgLinePointAdded(arg));
|
||||
} else if (entityType === 'inpaint_mask') {
|
||||
this.store.dispatch(imLinePointAdded(arg));
|
||||
}
|
||||
};
|
||||
onRectShapeAdded = (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => {
|
||||
this.log('Rect shape added');
|
||||
if (entityType === 'layer') {
|
||||
this.store.dispatch(layerRectAdded(arg));
|
||||
} else if (entityType === 'regional_guidance') {
|
||||
this.store.dispatch(rgRectAdded(arg));
|
||||
} else if (entityType === 'inpaint_mask') {
|
||||
this.store.dispatch(imRectAdded(arg));
|
||||
}
|
||||
};
|
||||
onBboxTransformed = (bbox: IRect) => {
|
||||
this.log('Generation bbox transformed');
|
||||
this.store.dispatch(bboxChanged(bbox));
|
||||
};
|
||||
onBrushWidthChanged = (width: number) => {
|
||||
this.log('Brush width changed');
|
||||
this.store.dispatch(brushWidthChanged(width));
|
||||
};
|
||||
onEraserWidthChanged = (width: number) => {
|
||||
this.log('Eraser width changed');
|
||||
this.store.dispatch(eraserWidthChanged(width));
|
||||
};
|
||||
onRegionMaskImageCached = (id: string, imageDTO: ImageDTO) => {
|
||||
this.log('Region mask image cached');
|
||||
this.store.dispatch(rgImageCacheChanged({ id, imageDTO }));
|
||||
};
|
||||
onInpaintMaskImageCached = (imageDTO: ImageDTO) => {
|
||||
this.log('Inpaint mask image cached');
|
||||
this.store.dispatch(imImageCacheChanged({ imageDTO }));
|
||||
};
|
||||
onLayerImageCached = (imageDTO: ImageDTO) => {
|
||||
this.log('Layer image cached');
|
||||
this.store.dispatch(layerImageCacheChanged({ imageDTO }));
|
||||
};
|
||||
setTool = (tool: Tool) => {
|
||||
this.log('Tool selection changed');
|
||||
this.store.dispatch(toolChanged(tool));
|
||||
};
|
||||
setToolBuffer = (toolBuffer: Tool | null) => {
|
||||
this.log('Tool buffer changed');
|
||||
this.store.dispatch(toolBufferChanged(toolBuffer));
|
||||
};
|
||||
|
||||
getSelectedEntity = (): CanvasEntity | null => {
|
||||
const state = this.getState();
|
||||
const identifier = state.selectedEntityIdentifier;
|
||||
let selectedEntity: CanvasEntity | null = null;
|
||||
if (!identifier) {
|
||||
selectedEntity = null;
|
||||
} else if (identifier.type === 'layer') {
|
||||
selectedEntity = state.layers.entities.find((i) => i.id === identifier.id) ?? null;
|
||||
} else if (identifier.type === 'control_adapter') {
|
||||
selectedEntity = state.controlAdapters.entities.find((i) => i.id === identifier.id) ?? null;
|
||||
} else if (identifier.type === 'ip_adapter') {
|
||||
selectedEntity = state.ipAdapters.entities.find((i) => i.id === identifier.id) ?? null;
|
||||
} else if (identifier.type === 'regional_guidance') {
|
||||
selectedEntity = state.regions.entities.find((i) => i.id === identifier.id) ?? null;
|
||||
} else if (identifier.type === 'inpaint_mask') {
|
||||
selectedEntity = state.inpaintMask;
|
||||
} else {
|
||||
selectedEntity = null;
|
||||
}
|
||||
return selectedEntity;
|
||||
};
|
||||
|
||||
getCurrentFill = () => {
|
||||
const state = this.getState();
|
||||
const selectedEntity = this.getSelectedEntity();
|
||||
let currentFill: RgbaColor = state.tool.fill;
|
||||
if (selectedEntity) {
|
||||
if (selectedEntity.type === 'regional_guidance') {
|
||||
currentFill = { ...selectedEntity.fill, a: state.settings.maskOpacity };
|
||||
} else if (selectedEntity.type === 'inpaint_mask') {
|
||||
currentFill = { ...state.inpaintMask.fill, a: state.settings.maskOpacity };
|
||||
}
|
||||
} else {
|
||||
currentFill = state.tool.fill;
|
||||
}
|
||||
return currentFill;
|
||||
};
|
||||
getBbox = () => {
|
||||
return this.getState().bbox;
|
||||
};
|
||||
getDocument = () => {
|
||||
return this.getState().document;
|
||||
};
|
||||
getToolState = () => {
|
||||
return this.getState().tool;
|
||||
};
|
||||
getSettings = () => {
|
||||
return this.getState().settings;
|
||||
};
|
||||
getRegionsState = () => {
|
||||
return this.getState().regions;
|
||||
};
|
||||
getLayersState = () => {
|
||||
return this.getState().layers;
|
||||
};
|
||||
getControlAdaptersState = () => {
|
||||
return this.getState().controlAdapters;
|
||||
};
|
||||
getInpaintMaskState = () => {
|
||||
return this.getState().inpaintMask;
|
||||
};
|
||||
getMaskOpacity = () => {
|
||||
return this.getState().settings.maskOpacity;
|
||||
};
|
||||
getStagingAreaState = () => {
|
||||
return this.getState().stagingArea;
|
||||
};
|
||||
getIsSelected = (id: string) => {
|
||||
return this.getSelectedEntity()?.id === id;
|
||||
};
|
||||
|
||||
// Read-only state, derived from nanostores
|
||||
resetLastProgressEvent = () => {
|
||||
$lastProgressEvent.set(null);
|
||||
};
|
||||
|
||||
// Read-write state, ephemeral interaction state
|
||||
getIsDrawing = $isDrawing.get;
|
||||
setIsDrawing = $isDrawing.set;
|
||||
|
||||
getIsMouseDown = $isMouseDown.get;
|
||||
setIsMouseDown = $isMouseDown.set;
|
||||
|
||||
getLastAddedPoint = $lastAddedPoint.get;
|
||||
setLastAddedPoint = $lastAddedPoint.set;
|
||||
|
||||
getLastMouseDownPos = $lastMouseDownPos.get;
|
||||
setLastMouseDownPos = $lastMouseDownPos.set;
|
||||
|
||||
getLastCursorPos = $lastCursorPos.get;
|
||||
setLastCursorPos = $lastCursorPos.set;
|
||||
|
||||
getSpaceKey = $spaceKey.get;
|
||||
setSpaceKey = $spaceKey.set;
|
||||
|
||||
getLastProgressEvent = $lastProgressEvent.get;
|
||||
setLastProgressEvent = $lastProgressEvent.set;
|
||||
|
||||
getAltKey = $alt.get;
|
||||
getCtrlKey = $ctrl.get;
|
||||
getMetaKey = $meta.get;
|
||||
getShiftKey = $shift.get;
|
||||
|
||||
getShouldShowStagedImage = $shouldShowStagedImage.get;
|
||||
setStageAttrs = $stageAttrs.set;
|
||||
}
|
@ -5,10 +5,11 @@ import {
|
||||
BRUSH_ERASER_BORDER_WIDTH,
|
||||
} from 'features/controlLayers/konva/constants';
|
||||
import { PREVIEW_RECT_ID } from 'features/controlLayers/konva/naming';
|
||||
import type { CanvasEntity, CanvasV2State, Position, RgbaColor } from 'features/controlLayers/store/types';
|
||||
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||
import Konva from 'konva';
|
||||
|
||||
export class CanvasTool {
|
||||
manager: KonvaNodeManager;
|
||||
group: Konva.Group;
|
||||
brush: {
|
||||
group: Konva.Group;
|
||||
@ -27,7 +28,8 @@ export class CanvasTool {
|
||||
fillRect: Konva.Rect;
|
||||
};
|
||||
|
||||
constructor() {
|
||||
constructor(manager: KonvaNodeManager) {
|
||||
this.manager = manager;
|
||||
this.group = new Konva.Group();
|
||||
|
||||
// Create the brush preview group & circles
|
||||
@ -94,8 +96,9 @@ export class CanvasTool {
|
||||
this.group.add(this.rect.group);
|
||||
}
|
||||
|
||||
scaleTool(stage: Konva.Stage, toolState: CanvasV2State['tool']) {
|
||||
const scale = stage.scaleX();
|
||||
scaleTool = () => {
|
||||
const toolState = this.manager.stateApi.getToolState();
|
||||
const scale = this.manager.stage.scaleX();
|
||||
|
||||
const brushRadius = toolState.brush.width / 2;
|
||||
this.brush.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale);
|
||||
@ -110,19 +113,19 @@ export class CanvasTool {
|
||||
strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale,
|
||||
radius: eraserRadius + BRUSH_ERASER_BORDER_WIDTH / scale,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const stage = this.manager.stage;
|
||||
const renderedEntityCount: number = 1; // TODO(psyche): this.manager should be renderable entity count
|
||||
const toolState = this.manager.stateApi.getToolState();
|
||||
const currentFill = this.manager.stateApi.getCurrentFill();
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntity();
|
||||
const cursorPos = this.manager.stateApi.getLastCursorPos();
|
||||
const lastMouseDownPos = this.manager.stateApi.getLastMouseDownPos();
|
||||
const isDrawing = this.manager.stateApi.getIsDrawing();
|
||||
const isMouseDown = this.manager.stateApi.getIsMouseDown();
|
||||
|
||||
render(
|
||||
stage: Konva.Stage,
|
||||
renderedEntityCount: number,
|
||||
toolState: CanvasV2State['tool'],
|
||||
currentFill: RgbaColor,
|
||||
selectedEntity: CanvasEntity | null,
|
||||
cursorPos: Position | null,
|
||||
lastMouseDownPos: Position | null,
|
||||
isDrawing: boolean,
|
||||
isMouseDown: boolean
|
||||
) {
|
||||
const tool = toolState.selected;
|
||||
const isDrawableEntity =
|
||||
selectedEntity?.type === 'regional_guidance' ||
|
||||
@ -182,7 +185,7 @@ export class CanvasTool {
|
||||
radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale,
|
||||
});
|
||||
|
||||
this.scaleTool(stage, toolState);
|
||||
this.scaleTool();
|
||||
|
||||
this.brush.group.visible(true);
|
||||
this.eraser.group.visible(false);
|
||||
@ -208,7 +211,7 @@ export class CanvasTool {
|
||||
radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale,
|
||||
});
|
||||
|
||||
this.scaleTool(stage, toolState);
|
||||
this.scaleTool();
|
||||
|
||||
this.brush.group.visible(false);
|
||||
this.eraser.group.visible(true);
|
@ -1,6 +1,6 @@
|
||||
import {
|
||||
CA_LAYER_NAME,
|
||||
INPAINT_MASK_LAYER_NAME,
|
||||
INPAINT_MASK_LAYER_ID,
|
||||
RASTER_LAYER_BRUSH_LINE_NAME,
|
||||
RASTER_LAYER_ERASER_LINE_NAME,
|
||||
RASTER_LAYER_IMAGE_NAME,
|
||||
@ -102,7 +102,7 @@ export const selectRenderableLayers = (node: Konva.Node): boolean =>
|
||||
node.name() === RG_LAYER_NAME ||
|
||||
node.name() === CA_LAYER_NAME ||
|
||||
node.name() === RASTER_LAYER_NAME ||
|
||||
node.name() === INPAINT_MASK_LAYER_NAME;
|
||||
node.name() === INPAINT_MASK_LAYER_ID;
|
||||
|
||||
/**
|
||||
* Konva selection callback to select RG mask objects. This includes lines and rects.
|
||||
|
@ -3,6 +3,7 @@ import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import { INPAINT_MASK_LAYER_ID } from 'features/controlLayers/konva/naming';
|
||||
import { bboxReducers } from 'features/controlLayers/store/bboxReducers';
|
||||
import { compositingReducers } from 'features/controlLayers/store/compositingReducers';
|
||||
import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers';
|
||||
@ -20,19 +21,19 @@ import type { AspectRatioState } from 'features/parameters/components/ImageSize/
|
||||
import { atom } from 'nanostores';
|
||||
import type { InvocationDenoiseProgressEvent } from 'services/events/types';
|
||||
|
||||
import type { CanvasEntityIdentifier, CanvasV2State, StageAttrs } from './types';
|
||||
import type { CanvasEntityIdentifier, CanvasV2State, Position, StageAttrs } from './types';
|
||||
import { RGBA_RED } from './types';
|
||||
|
||||
const initialState: CanvasV2State = {
|
||||
_version: 3,
|
||||
selectedEntityIdentifier: { type: 'inpaint_mask', id: 'inpaint_mask' },
|
||||
selectedEntityIdentifier: { type: 'inpaint_mask', id: INPAINT_MASK_LAYER_ID },
|
||||
layers: { entities: [], imageCache: null },
|
||||
controlAdapters: { entities: [] },
|
||||
ipAdapters: { entities: [] },
|
||||
regions: { entities: [] },
|
||||
loras: [],
|
||||
inpaintMask: {
|
||||
id: 'inpaint_mask',
|
||||
id: INPAINT_MASK_LAYER_ID,
|
||||
type: 'inpaint_mask',
|
||||
bbox: null,
|
||||
bboxNeedsUpdate: false,
|
||||
@ -366,6 +367,12 @@ export const $stageAttrs = atom<StageAttrs>({
|
||||
});
|
||||
export const $shouldShowStagedImage = atom(true);
|
||||
export const $lastProgressEvent = atom<InvocationDenoiseProgressEvent | null>(null);
|
||||
export const $isDrawing = atom<boolean>(false);
|
||||
export const $isMouseDown = atom<boolean>(false);
|
||||
export const $lastAddedPoint = atom<Position | null>(null);
|
||||
export const $lastMouseDownPos = atom<Position | null>(null);
|
||||
export const $lastCursorPos = atom<Position | null>(null);
|
||||
export const $spaceKey = atom<boolean>(false);
|
||||
|
||||
export const canvasV2PersistConfig: PersistConfig<CanvasV2State> = {
|
||||
name: canvasV2Slice.name,
|
||||
|
Loading…
Reference in New Issue
Block a user