From bd5a85bf70409e0135a4d9353a3e18693ebc4ad8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 21 Jun 2024 14:47:57 +1000 Subject: [PATCH] feat(ui): even more simplified API - lean on the konva node manager to abstract imperative state API & rendering --- .../features/controlLayers/konva/events.ts | 155 +++++++----------- .../controlLayers/konva/nodeManager.ts | 94 +++++++++-- .../controlLayers/konva/renderers/arrange.ts | 22 +-- .../konva/renderers/background.ts | 139 ++++++++-------- .../konva/renderers/controlAdapters.ts | 24 +-- .../konva/renderers/inpaintMask.ts | 31 ++-- .../controlLayers/konva/renderers/layers.ts | 27 +-- .../controlLayers/konva/renderers/preview.ts | 72 ++++---- .../controlLayers/konva/renderers/regions.ts | 28 +--- .../controlLayers/konva/renderers/renderer.ts | 124 ++++++-------- .../controlLayers/konva/renderers/stage.ts | 46 ++++-- 11 files changed, 371 insertions(+), 391 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 955162cd4c..6b8aa4bf7d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,16 +1,6 @@ import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; -import type { - BrushLineAddedArg, - CanvasEntity, - CanvasV2State, - EraserLineAddedArg, - PointAddedToLineArg, - RectShapeAddedArg, - RgbaColor, - StageAttrs, - Tool, -} from 'features/controlLayers/store/types'; +import type { CanvasEntity } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import type { Vector2d } from 'konva/lib/types'; import { clamp } from 'lodash-es'; @@ -25,43 +15,16 @@ import { } from './constants'; import { PREVIEW_TOOL_GROUP_ID } from './naming'; -type Arg = { - manager: KonvaNodeManager; - 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; - 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; -}; - /** * Updates the last cursor position atom with the current cursor position, returning the new position or `null` if the * cursor is not over the stage. * @param stage The konva stage * @param setLastCursorPos The callback to store the cursor pos */ -const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: Arg['setLastCursorPos']) => { +const updateLastCursorPos = ( + stage: Konva.Stage, + setLastCursorPos: KonvaNodeManager['stateApi']['setLastCursorPos'] +) => { const pos = getScaledFlooredCursorPosition(stage); if (!pos) { return null; @@ -93,10 +56,10 @@ const calculateNewBrushSize = (brushSize: number, delta: number) => { const maybeAddNextPoint = ( selectedEntity: CanvasEntity, currentPos: Vector2d, - getToolState: Arg['getToolState'], - getLastAddedPoint: Arg['getLastAddedPoint'], - setLastAddedPoint: Arg['setLastAddedPoint'], - onPointAddedToLine: Arg['onPointAddedToLine'] + getToolState: KonvaNodeManager['stateApi']['getToolState'], + getLastAddedPoint: KonvaNodeManager['stateApi']['getLastAddedPoint'], + setLastAddedPoint: KonvaNodeManager['stateApi']['setLastAddedPoint'], + onPointAddedToLine: KonvaNodeManager['stateApi']['onPointAddedToLine'] ) => { const isDrawableEntity = selectedEntity?.type === 'regional_guidance' || @@ -132,42 +95,42 @@ const maybeAddNextPoint = ( ); }; -export const setStageEventHandlers = ({ - manager, - getToolState, - getCurrentFill, - setTool, - setToolBuffer, - getIsDrawing, - setIsDrawing, - getIsMouseDown, - setIsMouseDown, - getLastMouseDownPos, - setLastMouseDownPos, - getLastCursorPos, - setLastCursorPos, - getLastAddedPoint, - setLastAddedPoint, - setStageAttrs, - getSelectedEntity, - getSpaceKey, - setSpaceKey, - getBbox, - getSettings, - onBrushLineAdded, - onEraserLineAdded, - onPointAddedToLine, - onRectShapeAdded, - onBrushWidthChanged: onBrushSizeChanged, - onEraserWidthChanged: onEraserSizeChanged, -}: Arg): (() => void) => { - const stage = manager.stage; +export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) => { + const { stage, stateApi } = manager; + const { + getToolState, + getCurrentFill, + setTool, + setToolBuffer, + getIsDrawing, + setIsDrawing, + getIsMouseDown, + setIsMouseDown, + getLastMouseDownPos, + setLastMouseDownPos, + getLastCursorPos, + setLastCursorPos, + getLastAddedPoint, + setLastAddedPoint, + setStageAttrs, + getSelectedEntity, + getSpaceKey, + setSpaceKey, + getBbox, + getSettings, + onBrushLineAdded, + onEraserLineAdded, + onPointAddedToLine, + onRectShapeAdded, + onBrushWidthChanged, + onEraserWidthChanged, + } = stateApi; //#region mouseenter stage.on('mouseenter', () => { const tool = getToolState().selected; stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); - manager.renderers.renderToolPreview(); + manager.konvaApi.renderToolPreview(); }); //#region mousedown @@ -288,7 +251,7 @@ export const setStageEventHandlers = ({ setLastAddedPoint(pos); } } - manager.renderers.renderToolPreview(); + manager.konvaApi.renderToolPreview(); }); //#region mouseup @@ -327,7 +290,7 @@ export const setStageEventHandlers = ({ setLastMouseDownPos(null); } - manager.renderers.renderToolPreview(); + manager.konvaApi.renderToolPreview(); }); //#region mousemove @@ -433,7 +396,7 @@ export const setStageEventHandlers = ({ } } } - manager.renderers.renderToolPreview(); + manager.konvaApi.renderToolPreview(); }); //#region mouseleave @@ -462,7 +425,7 @@ export const setStageEventHandlers = ({ } } - manager.renderers.renderToolPreview(); + manager.konvaApi.renderToolPreview(); }); //#region wheel @@ -477,9 +440,9 @@ export const setStageEventHandlers = ({ } // Holding ctrl or meta while scrolling changes the brush size if (toolState.selected === 'brush') { - onBrushSizeChanged(calculateNewBrushSize(toolState.brush.width, delta)); + onBrushWidthChanged(calculateNewBrushSize(toolState.brush.width, delta)); } else if (toolState.selected === 'eraser') { - onEraserSizeChanged(calculateNewBrushSize(toolState.eraser.width, delta)); + onEraserWidthChanged(calculateNewBrushSize(toolState.eraser.width, delta)); } } else { // We need the absolute cursor position - not the scaled position @@ -503,11 +466,11 @@ export const setStageEventHandlers = ({ stage.scaleY(newScale); stage.position(newPos); setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); - manager.renderers.renderBackground(); - manager.renderers.renderDocumentOverlay(); + manager.konvaApi.renderBackground(); + manager.konvaApi.renderDocumentOverlay(); } } - manager.renderers.renderToolPreview(); + manager.konvaApi.renderToolPreview(); }); //#region dragmove @@ -519,9 +482,9 @@ export const setStageEventHandlers = ({ height: stage.height(), scale: stage.scaleX(), }); - manager.renderers.renderBackground(); - manager.renderers.renderDocumentOverlay(); - manager.renderers.renderToolPreview(); + manager.konvaApi.renderBackground(); + manager.konvaApi.renderDocumentOverlay(); + manager.konvaApi.renderToolPreview(); }); //#region dragend @@ -534,7 +497,7 @@ export const setStageEventHandlers = ({ height: stage.height(), scale: stage.scaleX(), }); - manager.renderers.renderToolPreview(); + manager.konvaApi.renderToolPreview(); }); //#region key @@ -555,12 +518,12 @@ export const setStageEventHandlers = ({ setTool('view'); setSpaceKey(true); } else if (e.key === 'r') { - manager.renderers.fitDocumentToStage(); - manager.renderers.renderToolPreview(); - manager.renderers.renderBackground(); - manager.renderers.renderDocumentOverlay(); + manager.konvaApi.fitDocumentToStage(); + manager.konvaApi.renderToolPreview(); + manager.konvaApi.renderBackground(); + manager.konvaApi.renderDocumentOverlay(); } - manager.renderers.renderToolPreview(); + manager.konvaApi.renderToolPreview(); }; window.addEventListener('keydown', onKeyDown); @@ -578,7 +541,7 @@ export const setStageEventHandlers = ({ setToolBuffer(null); setSpaceKey(false); } - manager.renderers.renderToolPreview(); + manager.konvaApi.renderToolPreview(); }; window.addEventListener('keyup', onKeyUp); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index 1ac2e273c6..c093f87ee0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -1,5 +1,22 @@ -import type { BrushLine, CanvasEntity, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types'; +import type { + BrushLine, + BrushLineAddedArg, + CanvasEntity, + CanvasV2State, + EraserLine, + EraserLineAddedArg, + ImageObject, + PointAddedToLineArg, + PosChangedArg, + Rect, + RectShape, + RectShapeAddedArg, + RgbaColor, + StageAttrs, + Tool, +} from 'features/controlLayers/store/types'; import type Konva from 'konva'; +import type { Vector2d } from 'konva/lib/types'; import { assert } from 'tsafe'; export type BrushLineObjectRecord = { @@ -36,7 +53,7 @@ export type ImageObjectRecord = { type ObjectRecord = BrushLineObjectRecord | EraserLineObjectRecord | RectShapeObjectRecord | ImageObjectRecord; -type KonvaRenderers = { +type KonvaApi = { renderRegions: () => void; renderLayers: () => void; renderControlAdapters: () => void; @@ -45,8 +62,9 @@ type KonvaRenderers = { renderDocumentOverlay: () => void; renderBackground: () => void; renderToolPreview: () => void; - fitDocumentToStage: () => void; arrangeEntities: () => void; + fitDocumentToStage: () => void; + fitStageToContainer: () => void; }; type BackgroundLayer = { @@ -79,18 +97,63 @@ type PreviewLayer = { }; }; +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; + 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; + onPosChanged: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void; + onBboxTransformed: (bbox: Rect) => void; + getShiftKey: () => boolean; + getCtrlKey: () => boolean; + getMetaKey: () => boolean; + getAltKey: () => boolean; + getDocument: () => CanvasV2State['document']; + getLayerEntityStates: () => CanvasV2State['layers']['entities']; + getControlAdapterEntityStates: () => CanvasV2State['controlAdapters']['entities']; + getRegionEntityStates: () => CanvasV2State['regions']['entities']; + getInpaintMaskEntityState: () => CanvasV2State['inpaintMask']; +}; + export class KonvaNodeManager { stage: Konva.Stage; + container: HTMLDivElement; adapters: Map; _background: BackgroundLayer | null; _preview: PreviewLayer | null; - _renderers: KonvaRenderers | null; + _konvaApi: KonvaApi | null; + _stateApi: StateApi | null; - constructor(stage: Konva.Stage) { + constructor(stage: Konva.Stage, container: HTMLDivElement) { this.stage = stage; - this._renderers = null; + this.container = container; + this._konvaApi = null; this._preview = null; this._background = null; + this._stateApi = null; this.adapters = new Map(); } @@ -121,13 +184,13 @@ export class KonvaNodeManager { return this.adapters.delete(id); } - set renderers(renderers: KonvaRenderers) { - this._renderers = renderers; + set konvaApi(konvaApi: KonvaApi) { + this._konvaApi = konvaApi; } - get renderers(): KonvaRenderers { - assert(this._renderers !== null, 'Konva renderers have not been set'); - return this._renderers; + get konvaApi(): KonvaApi { + assert(this._konvaApi !== null, 'Konva API has not been set'); + return this._konvaApi; } set preview(preview: PreviewLayer) { @@ -147,6 +210,15 @@ export class KonvaNodeManager { assert(this._background !== null, 'Konva background layer has not been set'); return this._background; } + + set stateApi(stateApi: StateApi) { + this._stateApi = stateApi; + } + + get stateApi(): StateApi { + assert(this._stateApi !== null, 'State API has not been set'); + return this._stateApi; + } } export class KonvaEntityAdapter { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts index 6c040aaea5..dc640b10bb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts @@ -1,23 +1,14 @@ import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import type { CanvasV2State } from 'features/controlLayers/store/types'; /** * Gets a function to arrange the entities in the konva stage. * @param manager The konva node manager - * @param getLayerEntityStates A function to get all layer entity states - * @param getControlAdapterEntityStates A function to get all control adapter entity states - * @param getRegionEntityStates A function to get all region entity states * @returns An arrange entities function */ -export const getArrangeEntities = - (arg: { - manager: KonvaNodeManager; - getLayerEntityStates: () => CanvasV2State['layers']['entities']; - getControlAdapterEntityStates: () => CanvasV2State['controlAdapters']['entities']; - getRegionEntityStates: () => CanvasV2State['regions']['entities']; - }) => - (): void => { - const { manager, getLayerEntityStates, getControlAdapterEntityStates, getRegionEntityStates } = arg; +export const getArrangeEntities = (manager: KonvaNodeManager) => { + const { getLayerEntityStates, getControlAdapterEntityStates, getRegionEntityStates } = manager.stateApi; + + function arrangeEntities(): void { const layers = getLayerEntityStates(); const controlAdapters = getControlAdapterEntityStates(); const regions = getRegionEntityStates(); @@ -34,4 +25,7 @@ export const getArrangeEntities = } manager.get('inpaint_mask')?.konvaLayer.zIndex(++zIndex); manager.preview.layer.zIndex(++zIndex); - }; + } + + return arrangeEntities; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts index fe7ccebe92..15aa97e096 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts @@ -38,82 +38,85 @@ export const createBackgroundLayer = (): Konva.Layer => new Konva.Layer({ id: BA /** * Gets a render function for the background layer. - * @param arg.manager The konva node manager + * @param manager The konva node manager * @returns A function to render the background grid */ -export const getRenderBackground = (arg: { manager: KonvaNodeManager }) => (): void => { - const { manager } = arg; - const background = manager.background.layer; - background.zIndex(0); - const scale = manager.stage.scaleX(); - const gridSpacing = getGridSpacing(scale); - const x = manager.stage.x(); - const y = manager.stage.y(); - const width = manager.stage.width(); - const height = manager.stage.height(); - const stageRect = { - x1: 0, - y1: 0, - x2: width, - y2: height, - }; +export const getRenderBackground = (manager: KonvaNodeManager) => { + function renderBackground(): void { + const background = manager.background.layer; + background.zIndex(0); + const scale = manager.stage.scaleX(); + const gridSpacing = getGridSpacing(scale); + const x = manager.stage.x(); + const y = manager.stage.y(); + const width = manager.stage.width(); + const height = manager.stage.height(); + const stageRect = { + x1: 0, + y1: 0, + x2: width, + y2: height, + }; - const gridOffset = { - x: Math.ceil(x / scale / gridSpacing) * gridSpacing, - y: Math.ceil(y / scale / gridSpacing) * gridSpacing, - }; + const gridOffset = { + x: Math.ceil(x / scale / gridSpacing) * gridSpacing, + y: Math.ceil(y / scale / gridSpacing) * gridSpacing, + }; - const gridRect = { - x1: -gridOffset.x, - y1: -gridOffset.y, - x2: width / scale - gridOffset.x + gridSpacing, - y2: height / scale - gridOffset.y + gridSpacing, - }; + const gridRect = { + x1: -gridOffset.x, + y1: -gridOffset.y, + x2: width / scale - gridOffset.x + gridSpacing, + y2: height / scale - gridOffset.y + gridSpacing, + }; - const gridFullRect = { - x1: Math.min(stageRect.x1, gridRect.x1), - y1: Math.min(stageRect.y1, gridRect.y1), - x2: Math.max(stageRect.x2, gridRect.x2), - y2: Math.max(stageRect.y2, gridRect.y2), - }; + const gridFullRect = { + x1: Math.min(stageRect.x1, gridRect.x1), + y1: Math.min(stageRect.y1, gridRect.y1), + x2: Math.max(stageRect.x2, gridRect.x2), + y2: Math.max(stageRect.y2, gridRect.y2), + }; - // find the x & y size of the grid - const xSize = gridFullRect.x2 - gridFullRect.x1; - const ySize = gridFullRect.y2 - gridFullRect.y1; - // compute the number of steps required on each axis. - const xSteps = Math.round(xSize / gridSpacing) + 1; - const ySteps = Math.round(ySize / gridSpacing) + 1; + // find the x & y size of the grid + const xSize = gridFullRect.x2 - gridFullRect.x1; + const ySize = gridFullRect.y2 - gridFullRect.y1; + // compute the number of steps required on each axis. + const xSteps = Math.round(xSize / gridSpacing) + 1; + const ySteps = Math.round(ySize / gridSpacing) + 1; - const strokeWidth = 1 / scale; - let _x = 0; - let _y = 0; + const strokeWidth = 1 / scale; + let _x = 0; + let _y = 0; - background.destroyChildren(); + background.destroyChildren(); - for (let i = 0; i < xSteps; i++) { - _x = gridFullRect.x1 + i * gridSpacing; - background.add( - new Konva.Line({ - x: _x, - y: gridFullRect.y1, - points: [0, 0, 0, ySize], - stroke: _x % 64 ? fineGridLineColor : baseGridLineColor, - strokeWidth, - listening: false, - }) - ); - } - for (let i = 0; i < ySteps; i++) { - _y = gridFullRect.y1 + i * gridSpacing; - background.add( - new Konva.Line({ - x: gridFullRect.x1, - y: _y, - points: [0, 0, xSize, 0], - stroke: _y % 64 ? fineGridLineColor : baseGridLineColor, - strokeWidth, - listening: false, - }) - ); + for (let i = 0; i < xSteps; i++) { + _x = gridFullRect.x1 + i * gridSpacing; + background.add( + new Konva.Line({ + x: _x, + y: gridFullRect.y1, + points: [0, 0, 0, ySize], + stroke: _x % 64 ? fineGridLineColor : baseGridLineColor, + strokeWidth, + listening: false, + }) + ); + } + for (let i = 0; i < ySteps; i++) { + _y = gridFullRect.y1 + i * gridSpacing; + background.add( + new Konva.Line({ + x: gridFullRect.x1, + y: _y, + points: [0, 0, xSize, 0], + stroke: _y % 64 ? fineGridLineColor : baseGridLineColor, + strokeWidth, + listening: false, + }) + ); + } } + + return renderBackground; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts index 58b8f32922..d9db711147 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts @@ -6,16 +6,11 @@ import { createObjectGroup, updateImageSource, } from 'features/controlLayers/konva/renderers/objects'; -import type { CanvasV2State, ControlAdapterEntity } from 'features/controlLayers/store/types'; +import type { ControlAdapterEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { isEqual } from 'lodash-es'; import { assert } from 'tsafe'; -/** - * Logic for creating and rendering control adapter (control net & t2i adapter) layers. These layers have image objects - * and require some special handling to update the source and attributes as control images are swapped or processed. - */ - /** * Gets a control adapter entity's konva nodes and entity adapter, creating them if they do not exist. * @param manager The konva node manager @@ -102,16 +97,12 @@ export const renderControlAdapter = async (manager: KonvaNodeManager, entity: Co /** * Gets a function to render all control adapters. * @param manager The konva node manager - * @param getControlAdapterEntityStates A function to get all control adapter entities * @returns A function to render all control adapters */ -export const getRenderControlAdapters = - (arg: { - manager: KonvaNodeManager; - getControlAdapterEntityStates: () => CanvasV2State['controlAdapters']['entities']; - }) => - (): void => { - const { manager, getControlAdapterEntityStates } = arg; +export const getRenderControlAdapters = (manager: KonvaNodeManager) => { + const { getControlAdapterEntityStates } = manager.stateApi; + + function renderControlAdapters(): void { const entities = getControlAdapterEntityStates(); // Destroy nonexistent layers for (const adapters of manager.getAll('control_adapter')) { @@ -122,4 +113,7 @@ export const getRenderControlAdapters = for (const entity of entities) { renderControlAdapter(manager, entity); } - }; + } + + return renderControlAdapters; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts index ed1f155eae..3c1c103ff4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts @@ -16,7 +16,7 @@ import { getRectShape, } from 'features/controlLayers/konva/renderers/objects'; import { mapId } from 'features/controlLayers/konva/util'; -import type { CanvasEntity, CanvasV2State, InpaintMaskEntity, PosChangedArg } from 'features/controlLayers/store/types'; +import type { CanvasEntity, InpaintMaskEntity, PosChangedArg } from 'features/controlLayers/store/types'; import Konva from 'konva'; /** @@ -66,26 +66,14 @@ const getInpaintMask = ( }; /** - * Gets the inpaint mask render function. + * Gets a function to render the inpaint mask. * @param manager The konva node manager - * @param getEntityState A function to get the inpaint mask entity state - * @param getMaskOpacity A function to get the mask opacity - * @param getToolState A function to get the tool state - * @param getSelectedEntity A function to get the selected entity - * @param onPosChanged Callback for when the position changes (e.g. the entity is dragged) - * @returns The inpaint mask render function + * @returns A function to render the inpaint mask */ -export const getRenderInpaintMask = - (arg: { - manager: KonvaNodeManager; - getInpaintMaskEntityState: () => CanvasV2State['inpaintMask']; - getMaskOpacity: () => number; - getToolState: () => CanvasV2State['tool']; - getSelectedEntity: () => CanvasEntity | null; - onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void; - }) => - (): void => { - const { manager, getInpaintMaskEntityState, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = arg; +export const getRenderInpaintMask = (manager: KonvaNodeManager) => { + const { getInpaintMaskEntityState, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = manager.stateApi; + + function renderInpaintMask(): void { const entity = getInpaintMaskEntityState(); const globalMaskLayerOpacity = getMaskOpacity(); const toolState = getToolState(); @@ -228,4 +216,7 @@ export const getRenderInpaintMask = // } else { // bboxRect.visible(false); // } - }; + } + + return renderInpaintMask; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index a2ddceeef8..eff2f7de85 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -15,13 +15,9 @@ import { getRectShape, } from 'features/controlLayers/konva/renderers/objects'; import { mapId } from 'features/controlLayers/konva/util'; -import type { CanvasEntity, CanvasV2State, LayerEntity, PosChangedArg, Tool } from 'features/controlLayers/store/types'; +import type { CanvasEntity, LayerEntity, PosChangedArg, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; -/** - * Logic for creating and rendering raster layers. - */ - /** * Gets layer entity's konva nodes and entity adapter, creating them if they do not exist. * @param manager The konva node manager @@ -137,20 +133,12 @@ export const renderLayer = async ( /** * Gets a function to render all layers. * @param manager The konva node manager - * @param getLayerEntityStates A function to get all layer entities - * @param getToolState A function to get the current tool state - * @param onPosChanged Callback for when the layer's position changes * @returns A function to render all layers */ -export const getRenderLayers = - (arg: { - manager: KonvaNodeManager; - getLayerEntityStates: () => CanvasV2State['layers']['entities']; - getToolState: () => CanvasV2State['tool']; - onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void; - }) => - (): void => { - const { manager, getLayerEntityStates, getToolState, onPosChanged } = arg; +export const getRenderLayers = (manager: KonvaNodeManager) => { + const { getLayerEntityStates, getToolState, onPosChanged } = manager.stateApi; + + function renderLayers(): void { const entities = getLayerEntityStates(); const tool = getToolState(); // Destroy nonexistent layers @@ -162,4 +150,7 @@ export const getRenderLayers = for (const entity of entities) { renderLayer(manager, entity, tool.selected, onPosChanged); } - }; + } + + return renderLayers; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts index 18ff6bfb15..b5dcfb1df4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts @@ -19,9 +19,9 @@ import { PREVIEW_TOOL_GROUP_ID, } from 'features/controlLayers/konva/naming'; import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import type { CanvasEntity, CanvasV2State, RgbaColor } from 'features/controlLayers/store/types'; +import type { CanvasV2State } from 'features/controlLayers/store/types'; import Konva from 'konva'; -import type { IRect, Vector2d } from 'konva/lib/types'; +import type { IRect } from 'konva/lib/types'; import { atom } from 'nanostores'; /** @@ -245,13 +245,12 @@ const NO_ANCHORS: string[] = []; /** * Gets the bbox render function. * @param manager The konva node manager - * @param getBbox A function to get the bbox - * @param getToolState A function to get the tool state * @returns The bbox render function */ -export const getRenderBbox = - (manager: KonvaNodeManager, getBbox: () => CanvasV2State['bbox'], getToolState: () => CanvasV2State['tool']) => - (): void => { +export const getRenderBbox = (manager: KonvaNodeManager) => { + const { getBbox, getToolState } = manager.stateApi; + + return (): void => { const bbox = getBbox(); const toolState = getToolState(); manager.preview.bbox.group.listening(toolState.selected === 'bbox'); @@ -270,6 +269,7 @@ export const getRenderBbox = enabledAnchors: toolState.selected === 'bbox' ? ALL_ANCHORS : NO_ANCHORS, }); }; +}; /** * Gets the tool preview konva nodes. @@ -328,39 +328,21 @@ export const createToolPreviewNodes = (): KonvaNodeManager['preview']['tool'] => /** * Gets the tool preview (brush, eraser, rect) render function. - * @param arg.manager The konva node manager - * @param arg.getToolState The selected tool - * @param arg.currentFill The selected layer's color - * @param arg.selectedEntity The selected layer's type - * @param arg.globalMaskLayerOpacity The global mask layer opacity - * @param arg.cursorPos The cursor position - * @param arg.lastMouseDownPos The position of the last mouse down event - used for the rect tool - * @param arg.brushSize The brush size + * @param manager The konva node manager * @returns The tool preview render function */ -export const getRenderToolPreview = - (arg: { - manager: KonvaNodeManager; - getToolState: () => CanvasV2State['tool']; - getCurrentFill: () => RgbaColor; - getSelectedEntity: () => CanvasEntity | null; - getLastCursorPos: () => Vector2d | null; - getLastMouseDownPos: () => Vector2d | null; - getIsDrawing: () => boolean; - getIsMouseDown: () => boolean; - }) => - (): void => { - const { - manager, - getToolState, - getCurrentFill, - getSelectedEntity, - getLastCursorPos, - getLastMouseDownPos, - getIsDrawing, - getIsMouseDown, - } = arg; +export const getRenderToolPreview = (manager: KonvaNodeManager) => { + const { + getToolState, + getCurrentFill, + getSelectedEntity, + getLastCursorPos, + getLastMouseDownPos, + getIsDrawing, + getIsMouseDown, + } = manager.stateApi; + return (): void => { const stage = manager.stage; const layerCount = manager.adapters.size; const toolState = getToolState(); @@ -451,6 +433,7 @@ export const getRenderToolPreview = } } }; +}; /** * Scales the tool preview nodes. Depending on the scale of the stage, the border width and radius of the brush preview @@ -493,13 +476,13 @@ export const createDocumentOverlay = (): KonvaNodeManager['preview']['documentOv /** * Gets the document overlay render function. - * @param arg.manager The konva node manager - * @param arg.getDocument A function to get the document state + * @param manager The konva node manager * @returns The document overlay render function */ -export const getRenderDocumentOverlay = - (arg: { manager: KonvaNodeManager; getDocument: () => CanvasV2State['document'] }) => (): void => { - const { manager, getDocument } = arg; +export const getRenderDocumentOverlay = (manager: KonvaNodeManager) => { + const { getDocument } = manager.stateApi; + + function renderDocumentOverlay(): void { const document = getDocument(); const stage = manager.stage; @@ -524,4 +507,7 @@ export const getRenderDocumentOverlay = width: document.width, height: document.height, }); - }; + } + + return renderDocumentOverlay; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts index 79badb3e3f..a6087b320b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts @@ -19,7 +19,6 @@ import { mapId } from 'features/controlLayers/konva/util'; import type { CanvasEntity, CanvasEntityIdentifier, - CanvasV2State, PosChangedArg, RegionEntity, Tool, @@ -230,25 +229,13 @@ export const renderRegion = ( /** * Gets a function to render all regions. - * @param arg.manager The konva node manager - * @param arg.getRegionEntityStates A function to get all region entities - * @param arg.getMaskOpacity A function to get the mask opacity - * @param arg.getToolState A function to get the tool state - * @param arg.getSelectedEntity A function to get the selectedEntity - * @param arg.onPosChanged A callback for when the position of an entity changes + * @param manager The konva node manager * @returns A function to render all regions */ -export const getRenderRegions = - (arg: { - manager: KonvaNodeManager; - getRegionEntityStates: () => CanvasV2State['regions']['entities']; - getMaskOpacity: () => number; - getToolState: () => CanvasV2State['tool']; - getSelectedEntity: () => CanvasEntity | null; - onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void; - }) => - () => { - const { manager, getRegionEntityStates, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = arg; +export const getRenderRegions = (manager: KonvaNodeManager) => { + const { getRegionEntityStates, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = manager.stateApi; + + function renderRegions(): void { const entities = getRegionEntityStates(); const maskOpacity = getMaskOpacity(); const toolState = getToolState(); @@ -264,4 +251,7 @@ export const getRenderRegions = for (const entity of entities) { renderRegion(manager, entity, maskOpacity, toolState.selected, selectedEntity, onPosChanged); } - }; + } + + return renderRegions; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index 99115297f7..dcd876de01 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -21,7 +21,7 @@ import { getRenderToolPreview, } from 'features/controlLayers/konva/renderers/preview'; import { getRenderRegions } from 'features/controlLayers/konva/renderers/regions'; -import { getFitDocumentToStage } from 'features/controlLayers/konva/renderers/stage'; +import { getFitDocumentToStage, getFitStageToContainer } from 'features/controlLayers/konva/renderers/stage'; import { $stageAttrs, bboxChanged, @@ -283,7 +283,7 @@ export const initializeRenderer = ( spaceKey = val; }; - const manager = new KonvaNodeManager(stage); + const manager = new KonvaNodeManager(stage, container); $nodeManager.set(manager); manager.background = { layer: createBackgroundLayer() }; @@ -298,17 +298,31 @@ export const initializeRenderer = ( manager.preview.layer.add(manager.preview.tool.group); manager.preview.layer.add(manager.preview.documentOverlay.group); manager.stage.add(manager.preview.layer); - - const cleanupListeners = setStageEventHandlers({ - manager, + manager.stateApi = { + // Read-only state getToolState, + getSelectedEntity, + getBbox, + getSettings, + getCurrentFill, + getAltKey: $alt.get, + getCtrlKey: $ctrl.get, + getMetaKey: $meta.get, + getShiftKey: $shift.get, + getControlAdapterEntityStates, + getDocument, + getLayerEntityStates, + getRegionEntityStates, + getMaskOpacity, + getInpaintMaskEntityState, + + // Read-write state setTool, setToolBuffer, getIsDrawing, setIsDrawing, getIsMouseDown, setIsMouseDown, - getSelectedEntity, getLastAddedPoint, setLastAddedPoint, getLastCursorPos, @@ -318,61 +332,37 @@ export const initializeRenderer = ( getSpaceKey, setSpaceKey, setStageAttrs: $stageAttrs.set, - getBbox, - getSettings, + + // Callbacks onBrushLineAdded, onEraserLineAdded, onPointAddedToLine, onRectShapeAdded, onBrushWidthChanged, onEraserWidthChanged, - getCurrentFill, - }); + onPosChanged, + onBboxTransformed, + }; + + 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); - manager.renderers = { - renderRegions: getRenderRegions({ - manager, - getRegionEntityStates, - getMaskOpacity, - getToolState, - getSelectedEntity, - onPosChanged, - }), - renderLayers: getRenderLayers({ manager, getLayerEntityStates, getToolState, onPosChanged }), - renderControlAdapters: getRenderControlAdapters({ manager, getControlAdapterEntityStates }), - renderInpaintMask: getRenderInpaintMask({ - manager, - getInpaintMaskEntityState, - getMaskOpacity, - getToolState, - getSelectedEntity, - onPosChanged, - }), - renderBbox: getRenderBbox(manager, getBbox, getToolState), - renderToolPreview: getRenderToolPreview({ - manager, - getToolState, - getCurrentFill, - getSelectedEntity, - getLastCursorPos, - getLastMouseDownPos, - getIsDrawing, - getIsMouseDown, - }), - renderDocumentOverlay: getRenderDocumentOverlay({ manager, getDocument }), - renderBackground: getRenderBackground({ manager }), - fitDocumentToStage: getFitDocumentToStage({ manager, getDocument, setStageAttrs: $stageAttrs.set }), - arrangeEntities: getArrangeEntities({ - manager, - getLayerEntityStates, - getControlAdapterEntityStates, - getRegionEntityStates, - }), + manager.konvaApi = { + renderRegions: getRenderRegions(manager), + renderLayers: getRenderLayers(manager), + renderControlAdapters: getRenderControlAdapters(manager), + renderInpaintMask: getRenderInpaintMask(manager), + renderBbox: getRenderBbox(manager), + renderToolPreview: getRenderToolPreview(manager), + renderDocumentOverlay: getRenderDocumentOverlay(manager), + renderBackground: getRenderBackground(manager), + arrangeEntities: getArrangeEntities(manager), + fitDocumentToStage: getFitDocumentToStage(manager), + fitStageToContainer: getFitStageToContainer(manager), }; const renderCanvas = () => { @@ -392,7 +382,7 @@ export const initializeRenderer = ( canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering layers'); - manager.renderers.renderLayers(); + manager.konvaApi.renderLayers(); } if ( @@ -402,7 +392,7 @@ export const initializeRenderer = ( canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering regions'); - manager.renderers.renderRegions(); + manager.konvaApi.renderRegions(); } if ( @@ -412,22 +402,22 @@ export const initializeRenderer = ( canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering inpaint mask'); - manager.renderers.renderInpaintMask(); + manager.konvaApi.renderInpaintMask(); } if (isFirstRender || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities) { logIfDebugging('Rendering control adapters'); - manager.renderers.renderControlAdapters(); + manager.konvaApi.renderControlAdapters(); } if (isFirstRender || canvasV2.document !== prevCanvasV2.document) { logIfDebugging('Rendering document bounds overlay'); - manager.renderers.renderDocumentOverlay(); + manager.konvaApi.renderDocumentOverlay(); } if (isFirstRender || canvasV2.bbox !== prevCanvasV2.bbox || canvasV2.tool.selected !== prevCanvasV2.tool.selected) { logIfDebugging('Rendering generation bbox'); - manager.renderers.renderBbox(); + manager.konvaApi.renderBbox(); } if ( @@ -447,7 +437,7 @@ export const initializeRenderer = ( canvasV2.regions.entities !== prevCanvasV2.regions.entities ) { logIfDebugging('Arranging entities'); - manager.renderers.arrangeEntities(); + manager.konvaApi.arrangeEntities(); } prevCanvasV2 = canvasV2; @@ -461,30 +451,16 @@ export const initializeRenderer = ( // 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 fitStageToContainer = () => { - stage.width(container.offsetWidth); - stage.height(container.offsetHeight); - $stageAttrs.set({ - x: stage.x(), - y: stage.y(), - width: stage.width(), - height: stage.height(), - scale: stage.scaleX(), - }); - manager.renderers.renderBackground(); - manager.renderers.renderDocumentOverlay(); - }; - - const resizeObserver = new ResizeObserver(fitStageToContainer); + const resizeObserver = new ResizeObserver(manager.konvaApi.fitStageToContainer); resizeObserver.observe(container); - fitStageToContainer(); + manager.konvaApi.fitStageToContainer(); const unsubscribeRenderer = subscribe(renderCanvas); logIfDebugging('First render of konva stage'); // On first render, the document should be fit to the stage. - manager.renderers.fitDocumentToStage(); - manager.renderers.renderToolPreview(); + manager.konvaApi.fitDocumentToStage(); + manager.konvaApi.renderToolPreview(); renderCanvas(); return () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts index b4741c88ac..d02ecd485a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts @@ -1,23 +1,15 @@ import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants'; import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import type { CanvasV2State, StageAttrs } from 'features/controlLayers/store/types'; /** * Gets a function to fit the document to the stage, resetting the stage scale to 100%. * If the document is smaller than the stage, the stage scale is increased to fit the document. - * @param arg.manager The konva node manager - * @param arg.getDocument A function to get the current document state - * @param arg.setStageAttrs A function to set the stage attributes + * @param manager The konva node manager * @returns A function to fit the document to the stage */ -export const getFitDocumentToStage = - (arg: { - manager: KonvaNodeManager; - getDocument: () => CanvasV2State['document']; - setStageAttrs: (stageAttrs: StageAttrs) => void; - }) => - (): void => { - const { manager, getDocument, setStageAttrs } = arg; +export const getFitDocumentToStage = (manager: KonvaNodeManager) => { + function fitDocumentToStage(): void { + const { getDocument, setStageAttrs } = manager.stateApi; const document = getDocument(); // Fit & center the document on the stage const width = manager.stage.width(); @@ -29,4 +21,32 @@ export const getFitDocumentToStage = const y = (height - docHeightWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; manager.stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale }); setStageAttrs({ x, y, width, height, scale }); - }; + } + + return fitDocumentToStage; +}; + +/** + * Gets a function to fit the stage to its container element. Called during resize events. + * @param manager The konva node manager + * @returns A function to fit the stage to its container + */ +export const getFitStageToContainer = (manager: KonvaNodeManager) => { + const { stage, container } = manager; + const { setStageAttrs } = manager.stateApi; + function fitStageToContainer(): void { + stage.width(container.offsetWidth); + stage.height(container.offsetHeight); + setStageAttrs({ + x: stage.x(), + y: stage.y(), + width: stage.width(), + height: stage.height(), + scale: stage.scaleX(), + }); + manager.konvaApi.renderBackground(); + manager.konvaApi.renderDocumentOverlay(); + } + + return fitStageToContainer; +};