diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index cb2e1070e1..10fceda110 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -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]); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts b/invokeai/frontend/web/src/features/controlLayers/konva/background.ts similarity index 86% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/background.ts index 77dad03b27..2e8b3da177 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/background.ts @@ -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, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/bbox.ts similarity index 88% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/bbox.ts index f6e20708e3..f87689afff 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/bbox.ts @@ -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, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/konva/controlAdapters.ts similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/controlAdapters.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/documentSizeOverlay.ts b/invokeai/frontend/web/src/features/controlLayers/konva/documentSizeOverlay.ts similarity index 66% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/documentSizeOverlay.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/documentSizeOverlay.ts index abf87c485b..7259d2d6cb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/documentSizeOverlay.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/documentSizeOverlay.ts @@ -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 }); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/entityBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts similarity index 99% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/entityBbox.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts index 1669df159a..f0bb69bb32 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/entityBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts @@ -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, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 938743a735..c7c74e6105 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -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); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/inpaintMask.ts similarity index 95% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/inpaintMask.ts index 06d95993a3..4c7257879d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/inpaintMask.ts @@ -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; - 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()), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/layers.ts similarity index 99% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/layers.ts index 1935be1371..c902f24802 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/layers.ts @@ -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'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts index 0d35ed8631..c96464092c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts @@ -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'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index de5c372498..cdfde891f2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -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; @@ -116,41 +55,44 @@ export class KonvaNodeManager { stateApi: StateApi; preview: CanvasPreview; background: CanvasBackground; + private store: Store; + private isFirstRender: boolean; + private prevState: CanvasV2State; + private log: (message: string) => void; constructor( stage: Konva.Stage, container: HTMLDivElement, - stateApi: StateApi, + store: Store, + 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(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts b/invokeai/frontend/web/src/features/controlLayers/konva/objects.ts similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/objects.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/preview.ts similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/preview.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts b/invokeai/frontend/web/src/features/controlLayers/konva/regions.ts similarity index 97% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/regions.ts index 0f292dfaca..2d5ed1dd07 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/regions.ts @@ -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'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts deleted file mode 100644 index 997d564c88..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ /dev/null @@ -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, - 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(); - }; -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/stagingArea.ts similarity index 83% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/stagingArea.ts index 191d650904..02b1a50fdb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/stagingArea.ts @@ -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(); } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/stateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/stateApi.ts new file mode 100644 index 0000000000..e29b79b572 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/stateApi.ts @@ -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; + private log: (message: string) => void; + + constructor(store: Store, 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; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/tool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/tool.ts similarity index 88% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/tool.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/tool.ts index 38642b8f2e..e1e99b50c6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/tool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/tool.ts @@ -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); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 1b4d603e0f..ed4f70a6bb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -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. diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 0b63056b3d..c341e5e152 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -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({ }); export const $shouldShowStagedImage = atom(true); export const $lastProgressEvent = atom(null); +export const $isDrawing = atom(false); +export const $isMouseDown = atom(false); +export const $lastAddedPoint = atom(null); +export const $lastMouseDownPos = atom(null); +export const $lastCursorPos = atom(null); +export const $spaceKey = atom(false); export const canvasV2PersistConfig: PersistConfig = { name: canvasV2Slice.name,