diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index d3e7d54a92..3745046f20 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,3 +1,4 @@ +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; import { renderDocumentBoundsOverlay, @@ -32,7 +33,7 @@ import { import { PREVIEW_TOOL_GROUP_ID } from './naming'; type Arg = { - stage: Konva.Stage; + manager: KonvaNodeManager; getToolState: () => CanvasV2State['tool']; getCurrentFill: () => RgbaColor; setTool: (tool: Tool) => void; @@ -135,7 +136,7 @@ const maybeAddNextPoint = ( }; export const setStageEventHandlers = ({ - stage, + manager, getToolState, getCurrentFill, setTool, @@ -164,16 +165,14 @@ export const setStageEventHandlers = ({ onBrushWidthChanged: onBrushSizeChanged, onEraserWidthChanged: onEraserSizeChanged, }: Arg): (() => void) => { + const stage = manager.stage; + //#region mouseenter - stage.on('mouseenter', (e) => { - const stage = e.target.getStage(); - if (!stage) { - return; - } + stage.on('mouseenter', () => { const tool = getToolState().selected; stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); renderToolPreview( - stage, + manager, getToolState(), getCurrentFill(), getSelectedEntity(), @@ -186,10 +185,6 @@ export const setStageEventHandlers = ({ //#region mousedown stage.on('mousedown', (e) => { - const stage = e.target.getStage(); - if (!stage) { - return; - } setIsMouseDown(true); const toolState = getToolState(); const pos = updateLastCursorPos(stage, setLastCursorPos); @@ -307,7 +302,7 @@ export const setStageEventHandlers = ({ } } renderToolPreview( - stage, + manager, getToolState(), getCurrentFill(), getSelectedEntity(), @@ -319,11 +314,7 @@ export const setStageEventHandlers = ({ }); //#region mouseup - stage.on('mouseup', (e) => { - const stage = e.target.getStage(); - if (!stage) { - return; - } + stage.on('mouseup', () => { setIsMouseDown(false); const pos = getLastCursorPos(); const selectedEntity = getSelectedEntity(); @@ -360,7 +351,7 @@ export const setStageEventHandlers = ({ } renderToolPreview( - stage, + manager, getToolState(), getCurrentFill(), getSelectedEntity(), @@ -372,11 +363,7 @@ export const setStageEventHandlers = ({ }); //#region mousemove - stage.on('mousemove', (e) => { - const stage = e.target.getStage(); - if (!stage) { - return; - } + stage.on('mousemove', () => { const toolState = getToolState(); const pos = updateLastCursorPos(stage, setLastCursorPos); const selectedEntity = getSelectedEntity(); @@ -481,7 +468,7 @@ export const setStageEventHandlers = ({ } renderToolPreview( - stage, + manager, getToolState(), getCurrentFill(), getSelectedEntity(), @@ -493,11 +480,7 @@ export const setStageEventHandlers = ({ }); //#region mouseleave - stage.on('mouseleave', (e) => { - const stage = e.target.getStage(); - if (!stage) { - return; - } + stage.on('mouseleave', () => { const pos = updateLastCursorPos(stage, setLastCursorPos); setIsDrawing(false); setLastCursorPos(null); @@ -525,7 +508,7 @@ export const setStageEventHandlers = ({ } renderToolPreview( - stage, + manager, getToolState(), getCurrentFill(), getSelectedEntity(), @@ -574,13 +557,13 @@ export const setStageEventHandlers = ({ stage.scaleY(newScale); stage.position(newPos); setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); - renderBackgroundLayer(stage); - scaleToolPreview(stage, getToolState()); - renderDocumentBoundsOverlay(stage, getDocument); + renderBackgroundLayer(manager); + scaleToolPreview(manager, getToolState()); + renderDocumentBoundsOverlay(manager, getDocument); } } renderToolPreview( - stage, + manager, getToolState(), getCurrentFill(), getSelectedEntity(), @@ -600,10 +583,10 @@ export const setStageEventHandlers = ({ height: stage.height(), scale: stage.scaleX(), }); - renderBackgroundLayer(stage); - renderDocumentBoundsOverlay(stage, getDocument); + renderBackgroundLayer(manager); + renderDocumentBoundsOverlay(manager, getDocument); renderToolPreview( - stage, + manager, getToolState(), getCurrentFill(), getSelectedEntity(), @@ -625,7 +608,7 @@ export const setStageEventHandlers = ({ scale: stage.scaleX(), }); renderToolPreview( - stage, + manager, getToolState(), getCurrentFill(), getSelectedEntity(), @@ -656,12 +639,12 @@ export const setStageEventHandlers = ({ } else if (e.key === 'r') { const stageAttrs = fitDocumentToStage(stage, getDocument()); setStageAttrs(stageAttrs); - scaleToolPreview(stage, getToolState()); - renderBackgroundLayer(stage); - renderDocumentBoundsOverlay(stage, getDocument); + scaleToolPreview(manager, getToolState()); + renderBackgroundLayer(manager); + renderDocumentBoundsOverlay(manager, getDocument); } renderToolPreview( - stage, + manager, getToolState(), getCurrentFill(), getSelectedEntity(), @@ -688,7 +671,7 @@ export const setStageEventHandlers = ({ setSpaceKey(false); } renderToolPreview( - stage, + manager, getToolState(), getCurrentFill(), getSelectedEntity(), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index f96f1f9893..d1cba7bd71 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -1,4 +1,19 @@ -import type { BrushLine, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types'; +import { createBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; +import { + createBboxPreview, + createDocumentOverlay, + createPreviewLayer, + createToolPreview, +} from 'features/controlLayers/konva/renderers/preview'; +import type { + BrushLine, + CanvasEntity, + CanvasV2State, + EraserLine, + ImageObject, + Rect, + RectShape, +} from 'features/controlLayers/store/types'; import type Konva from 'konva'; export type BrushLineObjectRecord = { @@ -37,25 +52,77 @@ type ObjectRecord = BrushLineObjectRecord | EraserLineObjectRecord | RectShapeOb export class KonvaNodeManager { stage: Konva.Stage; - adapters: Map; + adapters: Map; + background: { layer: Konva.Layer }; + preview: { + layer: Konva.Layer; + bbox: { + group: Konva.Group; + rect: Konva.Rect; + transformer: Konva.Transformer; + }; + tool: { + group: Konva.Group; + brush: { + group: Konva.Group; + fill: Konva.Circle; + innerBorder: Konva.Circle; + outerBorder: Konva.Circle; + }; + rect: { + rect: Konva.Rect; + }; + }; + documentOverlay: { + group: Konva.Group; + innerRect: Konva.Rect; + outerRect: Konva.Rect; + }; + }; - constructor(stage: Konva.Stage) { + constructor( + stage: Konva.Stage, + getBbox: () => CanvasV2State['bbox'], + onBboxTransformed: (bbox: Rect) => void, + getShiftKey: () => boolean, + getCtrlKey: () => boolean, + getMetaKey: () => boolean, + getAltKey: () => boolean + ) { this.stage = stage; this.adapters = new Map(); + + this.background = { layer: createBackgroundLayer() }; + this.stage.add(this.background.layer); + + this.preview = { + layer: createPreviewLayer(), + bbox: createBboxPreview(stage, getBbox, onBboxTransformed, getShiftKey, getCtrlKey, getMetaKey, getAltKey), + tool: createToolPreview(stage), + documentOverlay: createDocumentOverlay(), + }; + this.preview.layer.add(this.preview.bbox.group); + this.preview.layer.add(this.preview.tool.group); + this.preview.layer.add(this.preview.documentOverlay.group); + this.stage.add(this.preview.layer); } - add(id: string, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group): EntityKonvaAdapter { - const adapter = new EntityKonvaAdapter(id, konvaLayer, konvaObjectGroup, this); - this.adapters.set(id, adapter); + add(entity: CanvasEntity, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group): KonvaEntityAdapter { + const adapter = new KonvaEntityAdapter(entity, konvaLayer, konvaObjectGroup, this); + this.adapters.set(adapter.id, adapter); return adapter; } - get(id: string): EntityKonvaAdapter | undefined { + get(id: string): KonvaEntityAdapter | undefined { return this.adapters.get(id); } - getAll(): EntityKonvaAdapter[] { - return Array.from(this.adapters.values()); + getAll(type?: CanvasEntity['type']): KonvaEntityAdapter[] { + if (type) { + return Array.from(this.adapters.values()).filter((adapter) => adapter.entityType === type); + } else { + return Array.from(this.adapters.values()); + } } destroy(id: string): boolean { @@ -68,15 +135,17 @@ export class KonvaNodeManager { } } -export class EntityKonvaAdapter { +export class KonvaEntityAdapter { id: string; + entityType: CanvasEntity['type']; konvaLayer: Konva.Layer; // Every entity is associated with a konva layer konvaObjectGroup: Konva.Group; // Every entity's nodes are part of an object group objectRecords: Map; manager: KonvaNodeManager; - constructor(id: string, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group, manager: KonvaNodeManager) { - this.id = id; + constructor(entity: CanvasEntity, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group, manager: KonvaNodeManager) { + this.id = entity.id; + this.entityType = entity.type; this.konvaLayer = konvaLayer; this.konvaObjectGroup = konvaObjectGroup; this.objectRecords = new Map(); 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 95bc6d005c..324d6ab5a5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts @@ -1,27 +1,22 @@ -import { BACKGROUND_LAYER_ID, PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming'; import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import type { ControlAdapterEntity, LayerEntity, RegionEntity } from 'features/controlLayers/store/types'; -import type Konva from 'konva'; export const arrangeEntities = ( - stage: Konva.Stage, - layerManager: KonvaNodeManager, + manager: KonvaNodeManager, layers: LayerEntity[], - controlAdapterManager: KonvaNodeManager, controlAdapters: ControlAdapterEntity[], - regionManager: KonvaNodeManager, regions: RegionEntity[] ): void => { let zIndex = 0; - stage.findOne(`#${BACKGROUND_LAYER_ID}`)?.zIndex(++zIndex); + manager.background.layer.zIndex(++zIndex); for (const layer of layers) { - layerManager.get(layer.id)?.konvaLayer.zIndex(++zIndex); + manager.get(layer.id)?.konvaLayer.zIndex(++zIndex); } for (const ca of controlAdapters) { - controlAdapterManager.get(ca.id)?.konvaLayer.zIndex(++zIndex); + manager.get(ca.id)?.konvaLayer.zIndex(++zIndex); } for (const rg of regions) { - regionManager.get(rg.id)?.konvaLayer.zIndex(++zIndex); + manager.get(rg.id)?.konvaLayer.zIndex(++zIndex); } - stage.findOne(`#${PREVIEW_LAYER_ID}`)?.zIndex(++zIndex); + manager.preview.layer.zIndex(++zIndex); }; 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 0f5c4ceaa5..9fff013070 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts @@ -1,5 +1,6 @@ import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; import { BACKGROUND_LAYER_ID } from 'features/controlLayers/konva/naming'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import Konva from 'konva'; const baseGridLineColor = getArbitraryBaseColor(27); @@ -24,26 +25,17 @@ const getGridSpacing = (scale: number): number => { return 256; }; -export const getBackgroundLayer = (stage: Konva.Stage): Konva.Layer => { - let background = stage.findOne(`#${BACKGROUND_LAYER_ID}`); - if (background) { - return background; - } +export const createBackgroundLayer = (): Konva.Layer => new Konva.Layer({ id: BACKGROUND_LAYER_ID, listening: false }); - background = new Konva.Layer({ id: BACKGROUND_LAYER_ID }); - stage.add(background); - return background; -}; - -export const renderBackgroundLayer = (stage: Konva.Stage): void => { - const background = getBackgroundLayer(stage); +export const renderBackgroundLayer = (manager: KonvaNodeManager): void => { + const background = manager.background.layer; background.zIndex(0); - const scale = stage.scaleX(); + const scale = 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 = manager.stage.x(); + const y = manager.stage.y(); + const width = manager.stage.width(); + const height = manager.stage.height(); const stageRect = { x1: 0, y1: 0, 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 6c8d908be6..952567a74d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts @@ -1,6 +1,6 @@ import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, CA_LAYER_OBJECT_GROUP_NAME } from 'features/controlLayers/konva/naming'; -import type { EntityKonvaAdapter, ImageObjectRecord, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { ImageObjectRecord, KonvaEntityAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { createImageObjectGroup, createObjectGroup, @@ -21,7 +21,7 @@ import { assert } from 'tsafe'; * @param stage The konva stage * @param entity The control adapter layer state */ -const getControlAdapter = (manager: KonvaNodeManager, entity: ControlAdapterEntity): EntityKonvaAdapter => { +const getControlAdapter = (manager: KonvaNodeManager, entity: ControlAdapterEntity): KonvaEntityAdapter => { const adapter = manager.get(entity.id); if (adapter) { return adapter; @@ -33,7 +33,7 @@ const getControlAdapter = (manager: KonvaNodeManager, entity: ControlAdapterEnti listening: false, }); const konvaObjectGroup = createObjectGroup(konvaLayer, CA_LAYER_OBJECT_GROUP_NAME); - return manager.add(entity.id, konvaLayer, konvaObjectGroup); + return manager.add(entity, konvaLayer, konvaObjectGroup); }; /** @@ -103,7 +103,7 @@ export const renderControlAdapter = async (manager: KonvaNodeManager, entity: Co export const renderControlAdapters = (manager: KonvaNodeManager, entities: ControlAdapterEntity[]): void => { // Destroy nonexistent layers - for (const adapters of manager.getAll()) { + for (const adapters of manager.getAll('control_adapter')) { if (!entities.find((ca) => ca.id === adapters.id)) { manager.destroy(adapters.id); } 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 a3c144f744..475692a599 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -6,7 +6,7 @@ import { RASTER_LAYER_OBJECT_GROUP_NAME, RASTER_LAYER_RECT_SHAPE_NAME, } from 'features/controlLayers/konva/naming'; -import type { EntityKonvaAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { KonvaEntityAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { createImageObjectGroup, createObjectGroup, @@ -29,11 +29,11 @@ import Konva from 'konva'; * @param onPosChanged Callback for when the layer's position changes */ const getLayer = ( - map: KonvaNodeManager, + manager: KonvaNodeManager, entity: LayerEntity, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -): EntityKonvaAdapter => { - const adapter = map.get(entity.id); +): KonvaEntityAdapter => { + const adapter = manager.get(entity.id); if (adapter) { return adapter; } @@ -54,7 +54,7 @@ const getLayer = ( } const konvaObjectGroup = createObjectGroup(konvaLayer, RASTER_LAYER_OBJECT_GROUP_NAME); - return map.add(entity.id, konvaLayer, konvaObjectGroup); + return manager.add(entity, konvaLayer, konvaObjectGroup); }; /** @@ -140,7 +140,7 @@ export const renderLayers = ( onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { // Destroy nonexistent layers - for (const adapter of manager.getAll()) { + for (const adapter of manager.getAll('layer')) { if (!entities.find((l) => l.id === adapter.id)) { manager.destroy(adapter.id); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts index 64c39b21d5..d1f46db6ce 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -8,7 +8,7 @@ import { } from 'features/controlLayers/konva/naming'; import type { BrushLineObjectRecord, - EntityKonvaAdapter, + KonvaEntityAdapter, EraserLineObjectRecord, ImageObjectRecord, RectShapeObjectRecord, @@ -40,7 +40,7 @@ import { v4 as uuidv4 } from 'uuid'; * @param name The konva name for the line */ export const getBrushLine = ( - adapter: EntityKonvaAdapter, + adapter: KonvaEntityAdapter, brushLine: BrushLine, name: string ): BrushLineObjectRecord => { @@ -75,7 +75,7 @@ export const getBrushLine = ( * @param name The konva name for the line */ export const getEraserLine = ( - adapter: EntityKonvaAdapter, + adapter: KonvaEntityAdapter, eraserLine: EraserLine, name: string ): EraserLineObjectRecord => { @@ -111,7 +111,7 @@ export const getEraserLine = ( * @param name The konva name for the rect */ export const getRectShape = ( - adapter: EntityKonvaAdapter, + adapter: KonvaEntityAdapter, rectShape: RectShape, name: string ): RectShapeObjectRecord => { @@ -203,7 +203,7 @@ export const updateImageSource = async (arg: { * @returns The konva group for the image placeholder, and callbacks to handle loading and error states */ export const createImageObjectGroup = (arg: { - adapter: EntityKonvaAdapter; + adapter: KonvaEntityAdapter; obj: ImageObject; name: string; getImageDTO?: (imageName: string) => Promise; 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 7416c3db20..c0b49ddbe0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts @@ -18,29 +18,15 @@ import { PREVIEW_RECT_ID, PREVIEW_TOOL_GROUP_ID, } from 'features/controlLayers/konva/naming'; -import { selectRenderableLayers } from 'features/controlLayers/konva/util'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import type { CanvasEntity, CanvasV2State, RgbaColor, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect, Vector2d } from 'konva/lib/types'; import { atom } from 'nanostores'; -import { assert } from 'tsafe'; -/** - * Creates the singleton preview layer and all its objects. - * @param stage The konva stage - */ -const getPreviewLayer = (stage: Konva.Stage): Konva.Layer => { - let previewLayer = stage.findOne(`#${PREVIEW_LAYER_ID}`); - if (previewLayer) { - return previewLayer; - } - // Initialize the preview layer & add to the stage - previewLayer = new Konva.Layer({ id: PREVIEW_LAYER_ID, listening: true }); - stage.add(previewLayer); - return previewLayer; -}; +export const createPreviewLayer = (): Konva.Layer => new Konva.Layer({ id: PREVIEW_LAYER_ID, listening: true }); -export const getBboxPreviewGroup = ( +export const createBboxPreview = ( stage: Konva.Stage, getBbox: () => IRect, onBboxTransformed: (bbox: IRect) => void, @@ -48,14 +34,7 @@ export const getBboxPreviewGroup = ( getCtrlKey: () => boolean, getMetaKey: () => boolean, getAltKey: () => boolean -): Konva.Group => { - const previewLayer = getPreviewLayer(stage); - let bboxPreviewGroup = previewLayer.findOne(`#${PREVIEW_GENERATION_BBOX_GROUP}`); - - if (bboxPreviewGroup) { - return bboxPreviewGroup; - } - +): { group: Konva.Group; rect: Konva.Rect; transformer: Konva.Transformer } => { // 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(); @@ -63,28 +42,28 @@ export const getBboxPreviewGroup = ( // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully // transparent rect for this purpose. - bboxPreviewGroup = new Konva.Group({ id: PREVIEW_GENERATION_BBOX_GROUP, listening: false }); - const bboxRect = new Konva.Rect({ + const group = new Konva.Group({ id: PREVIEW_GENERATION_BBOX_GROUP, listening: false }); + const rect = new Konva.Rect({ id: PREVIEW_GENERATION_BBOX_DUMMY_RECT, listening: false, strokeEnabled: false, draggable: true, ...getBbox(), }); - bboxRect.on('dragmove', () => { + rect.on('dragmove', () => { const gridSize = getCtrlKey() || getMetaKey() ? 8 : 64; const oldBbox = getBbox(); const newBbox: IRect = { ...oldBbox, - x: roundToMultiple(bboxRect.x(), gridSize), - y: roundToMultiple(bboxRect.y(), gridSize), + x: roundToMultiple(rect.x(), gridSize), + y: roundToMultiple(rect.y(), gridSize), }; - bboxRect.setAttrs(newBbox); + rect.setAttrs(newBbox); if (oldBbox.x !== newBbox.x || oldBbox.y !== newBbox.y) { onBboxTransformed(newBbox); } }); - const bboxTransformer = new Konva.Transformer({ + const transformer = new Konva.Transformer({ id: PREVIEW_GENERATION_BBOX_TRANSFORMER, borderDash: [5, 5], borderStroke: 'rgba(212,216,234,1)', @@ -136,11 +115,11 @@ export const getBboxPreviewGroup = ( }, }); - bboxTransformer.on('transform', () => { + transformer.on('transform', () => { // In the transform callback, we calculate the bbox's new dims and pos and update the konva object. // Some special handling is needed depending on the anchor being dragged. - const anchor = bboxTransformer.getActiveAnchor(); + const anchor = transformer.getActiveAnchor(); if (!anchor) { // Pretty sure we should always have an anchor here? return; @@ -163,14 +142,14 @@ export const getBboxPreviewGroup = ( } // The coords should be correct per the anchorDragBoundFunc. - let x = bboxRect.x(); - let y = bboxRect.y(); + let x = rect.x(); + let y = rect.y(); // Konva transforms by scaling the dims, not directly changing width and height. At this point, the width and height // *have not changed*, only the scale has changed. To get the final height, we need to scale the dims and then snap // them to the grid. - let width = roundToMultipleMin(bboxRect.width() * bboxRect.scaleX(), gridSize); - let height = roundToMultipleMin(bboxRect.height() * bboxRect.scaleY(), gridSize); + let width = roundToMultipleMin(rect.width() * rect.scaleX(), gridSize); + let height = roundToMultipleMin(rect.height() * rect.scaleY(), gridSize); // If shift is held and we are resizing from a corner, retain aspect ratio - needs special handling. We skip this // if alt/opt is held - this requires math too big for my brain. @@ -210,7 +189,7 @@ export const getBboxPreviewGroup = ( // Update the bboxRect's attrs directly with the new transform, and reset its scale to 1. // TODO(psyche): In `renderBboxPreview()` we also call setAttrs, need to do it twice to ensure it renders correctly. // Gotta be a way to avoid setting it twice... - bboxRect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 }); + rect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 }); // Update the bbox in internal state. onBboxTransformed(bbox); @@ -222,18 +201,17 @@ export const getBboxPreviewGroup = ( } }); - bboxTransformer.on('transformend', () => { + transformer.on('transformend', () => { // Always update the aspect ratio buffer when the transform ends, so if the next transform starts with shift held, // we have the correct aspect ratio to start from. - $aspectRatioBuffer.set(bboxRect.width() / bboxRect.height()); + $aspectRatioBuffer.set(rect.width() / rect.height()); }); // The transformer will always be transforming the dummy rect - bboxTransformer.nodes([bboxRect]); - bboxPreviewGroup.add(bboxRect); - bboxPreviewGroup.add(bboxTransformer); - previewLayer.add(bboxPreviewGroup); - return bboxPreviewGroup; + transformer.nodes([rect]); + group.add(rect); + group.add(transformer); + return { group, rect, transformer }; }; const ALL_ANCHORS: string[] = [ @@ -249,79 +227,66 @@ const ALL_ANCHORS: string[] = [ const CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; const NO_ANCHORS: string[] = []; -export const renderBboxPreview = ( - stage: Konva.Stage, - bbox: IRect, - tool: Tool, - getBbox: () => CanvasV2State['bbox'], - onBboxTransformed: (bbox: IRect) => void, - getShiftKey: () => boolean, - getCtrlKey: () => boolean, - getMetaKey: () => boolean, - getAltKey: () => boolean -): void => { - const bboxGroup = getBboxPreviewGroup( - stage, - getBbox, - onBboxTransformed, - getShiftKey, - getCtrlKey, - getMetaKey, - getAltKey - ); - const bboxRect = bboxGroup.findOne(`#${PREVIEW_GENERATION_BBOX_DUMMY_RECT}`); - const bboxTransformer = bboxGroup.findOne(`#${PREVIEW_GENERATION_BBOX_TRANSFORMER}`); - bboxGroup.listening(tool === 'bbox'); +export const renderBboxPreview = (manager: KonvaNodeManager, bbox: IRect, tool: Tool): void => { + manager.preview.bbox.group.listening(tool === 'bbox'); // This updates the bbox during transformation - bboxRect?.setAttrs({ ...bbox, scaleX: 1, scaleY: 1, listening: tool === 'bbox' }); - bboxTransformer?.setAttrs({ listening: tool === 'bbox', enabledAnchors: tool === 'bbox' ? ALL_ANCHORS : NO_ANCHORS }); + manager.preview.bbox.rect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1, listening: tool === 'bbox' }); + manager.preview.bbox.transformer.setAttrs({ + listening: tool === 'bbox', + enabledAnchors: tool === 'bbox' ? ALL_ANCHORS : NO_ANCHORS, + }); }; -export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { - const previewLayer = getPreviewLayer(stage); - let toolPreviewGroup = previewLayer.findOne(`#${PREVIEW_TOOL_GROUP_ID}`); - if (toolPreviewGroup) { - return toolPreviewGroup; - } +export const createToolPreview = (stage: Konva.Stage): KonvaNodeManager['preview']['tool'] => { const scale = stage.scaleX(); - toolPreviewGroup = new Konva.Group({ id: PREVIEW_TOOL_GROUP_ID }); + const group = new Konva.Group({ id: PREVIEW_TOOL_GROUP_ID }); // Create the brush preview group & circles - const brushPreviewGroup = new Konva.Group({ id: PREVIEW_BRUSH_GROUP_ID }); - const brushPreviewFill = new Konva.Circle({ + const brushGroup = new Konva.Group({ id: PREVIEW_BRUSH_GROUP_ID }); + const brushFill = new Konva.Circle({ id: PREVIEW_BRUSH_FILL_ID, listening: false, strokeEnabled: false, }); - brushPreviewGroup.add(brushPreviewFill); - const brushPreviewBorderInner = new Konva.Circle({ + brushGroup.add(brushFill); + const brushBorderInner = new Konva.Circle({ id: PREVIEW_BRUSH_BORDER_INNER_ID, listening: false, stroke: BRUSH_BORDER_INNER_COLOR, strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, strokeEnabled: true, }); - brushPreviewGroup.add(brushPreviewBorderInner); - const brushPreviewBorderOuter = new Konva.Circle({ + brushGroup.add(brushBorderInner); + const brushBorderOuter = new Konva.Circle({ id: PREVIEW_BRUSH_BORDER_OUTER_ID, listening: false, stroke: BRUSH_BORDER_OUTER_COLOR, strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, strokeEnabled: true, }); - brushPreviewGroup.add(brushPreviewBorderOuter); + brushGroup.add(brushBorderOuter); // Create the rect preview - this is a rectangle drawn from the last mouse down position to the current cursor position - const rectPreview = new Konva.Rect({ + const rect = new Konva.Rect({ id: PREVIEW_RECT_ID, listening: false, strokeEnabled: false, }); - toolPreviewGroup.add(rectPreview); - toolPreviewGroup.add(brushPreviewGroup); - previewLayer.add(toolPreviewGroup); - return toolPreviewGroup; + group.add(rect); + group.add(brushGroup); + return { + group, + brush: { + group: brushGroup, + fill: brushFill, + innerBorder: brushBorderInner, + outerBorder: brushBorderOuter, + }, + rect: { + rect, + }, + }; }; /** @@ -336,7 +301,7 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { * @param brushSize The brush size */ export const renderToolPreview = ( - stage: Konva.Stage, + manager: KonvaNodeManager, toolState: CanvasV2State['tool'], currentFill: RgbaColor, selectedEntity: CanvasEntity | null, @@ -345,7 +310,8 @@ export const renderToolPreview = ( isDrawing: boolean, isMouseDown: boolean ): void => { - const layerCount = stage.find(selectRenderableLayers).length; + const stage = manager.stage; + const layerCount = manager.adapters.size; const tool = toolState.selected; // Update the stage's pointer style if (tool === 'view') { @@ -372,31 +338,22 @@ export const renderToolPreview = ( stage.draggable(tool === 'view'); - const toolPreviewGroup = getToolPreviewGroup(stage); - if ( !cursorPos || layerCount === 0 || (selectedEntity?.type !== 'regional_guidance' && selectedEntity?.type !== 'layer') ) { // We can bail early if the mouse isn't over the stage or there are no layers - toolPreviewGroup.visible(false); + manager.preview.tool.group.visible(false); } else { - toolPreviewGroup.visible(true); - - const brushPreviewGroup = stage.findOne(`#${PREVIEW_BRUSH_GROUP_ID}`); - assert(brushPreviewGroup, 'Brush preview group not found'); - - const rectPreview = stage.findOne(`#${PREVIEW_RECT_ID}`); - assert(rectPreview, 'Rect preview not found'); + manager.preview.tool.group.visible(true); // No need to render the brush preview if the cursor position or color is missing if (cursorPos && (tool === 'brush' || tool === 'eraser')) { const scale = stage.scaleX(); // Update the fill circle - const brushPreviewFill = brushPreviewGroup.findOne(`#${PREVIEW_BRUSH_FILL_ID}`); const radius = (tool === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2; - brushPreviewFill?.setAttrs({ + manager.preview.tool.brush.fill.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius, @@ -405,83 +362,74 @@ export const renderToolPreview = ( }); // Update the inner border of the brush preview - const brushPreviewInner = brushPreviewGroup.findOne(`#${PREVIEW_BRUSH_BORDER_INNER_ID}`); - brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); + manager.preview.tool.brush.innerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); // Update the outer border of the brush preview - const brushPreviewOuter = brushPreviewGroup.findOne(`#${PREVIEW_BRUSH_BORDER_OUTER_ID}`); - brushPreviewOuter?.setAttrs({ + manager.preview.tool.brush.outerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, }); - scaleToolPreview(stage, toolState); + scaleToolPreview(manager, toolState); - brushPreviewGroup.visible(true); + manager.preview.tool.brush.group.visible(true); } else { - brushPreviewGroup.visible(false); + manager.preview.tool.brush.group.visible(false); } if (cursorPos && lastMouseDownPos && tool === 'rect') { - const rectPreview = toolPreviewGroup.findOne(`#${PREVIEW_RECT_ID}`); - rectPreview?.setAttrs({ + manager.preview.tool.rect.rect.setAttrs({ x: Math.min(cursorPos.x, lastMouseDownPos.x), y: Math.min(cursorPos.y, lastMouseDownPos.y), width: Math.abs(cursorPos.x - lastMouseDownPos.x), height: Math.abs(cursorPos.y - lastMouseDownPos.y), fill: rgbaColorToString(currentFill), + visible: true, }); - rectPreview?.visible(true); } else { - rectPreview?.visible(false); + manager.preview.tool.rect.rect.visible(false); } } }; -export const scaleToolPreview = (stage: Konva.Stage, toolState: CanvasV2State['tool']): void => { - const scale = stage.scaleX(); +export const scaleToolPreview = (manager: KonvaNodeManager, toolState: CanvasV2State['tool']): void => { + const scale = manager.stage.scaleX(); const radius = (toolState.selected === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2; - const brushPreviewGroup = stage.findOne(`#${PREVIEW_BRUSH_GROUP_ID}`); - brushPreviewGroup - ?.findOne(`#${PREVIEW_BRUSH_BORDER_INNER_ID}`) - ?.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); - brushPreviewGroup - ?.findOne(`#${PREVIEW_BRUSH_BORDER_OUTER_ID}`) - ?.setAttrs({ strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale }); + manager.preview.tool.brush.innerBorder.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); + manager.preview.tool.brush.outerBorder.setAttrs({ + strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, + radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, + }); }; -const getDocumentOverlayGroup = (stage: Konva.Stage): Konva.Group => { - const previewLayer = getPreviewLayer(stage); - let documentOverlayGroup = previewLayer.findOne('#document_overlay_group'); - if (documentOverlayGroup) { - return documentOverlayGroup; - } - - documentOverlayGroup = new Konva.Group({ id: 'document_overlay_group', listening: false }); - const documentOverlayOuterRect = new Konva.Rect({ +export const createDocumentOverlay = (): KonvaNodeManager['preview']['documentOverlay'] => { + const group = new Konva.Group({ id: 'document_overlay_group', listening: false }); + const outerRect = new Konva.Rect({ id: 'document_overlay_outer_rect', listening: false, fill: getArbitraryBaseColor(10), opacity: 0.7, }); - const documentOverlayInnerRect = new Konva.Rect({ + const innerRect = new Konva.Rect({ id: 'document_overlay_inner_rect', listening: false, fill: 'white', globalCompositeOperation: 'destination-out', }); - documentOverlayGroup.add(documentOverlayOuterRect); - documentOverlayGroup.add(documentOverlayInnerRect); - previewLayer.add(documentOverlayGroup); - return documentOverlayGroup; + group.add(outerRect); + group.add(innerRect); + return { group, innerRect, outerRect }; }; -export const renderDocumentBoundsOverlay = (stage: Konva.Stage, getDocument: () => CanvasV2State['document']): void => { +export const renderDocumentBoundsOverlay = ( + manager: KonvaNodeManager, + getDocument: () => CanvasV2State['document'] +): void => { const document = getDocument(); - const documentOverlayGroup = getDocumentOverlayGroup(stage); + const stage = manager.stage; - documentOverlayGroup.zIndex(0); + manager.preview.documentOverlay.group.zIndex(0); const x = stage.x(); const y = stage.y(); @@ -489,14 +437,14 @@ export const renderDocumentBoundsOverlay = (stage: Konva.Stage, getDocument: () const height = stage.height(); const scale = stage.scaleX(); - documentOverlayGroup.findOne('#document_overlay_outer_rect')?.setAttrs({ + manager.preview.documentOverlay.outerRect.setAttrs({ offsetX: x / scale, offsetY: y / scale, width: width / scale, height: height / scale, }); - documentOverlayGroup.findOne('#document_overlay_inner_rect')?.setAttrs({ + manager.preview.documentOverlay.innerRect.setAttrs({ x: 0, y: 0, width: document.width, 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 7e5f4b748a..ae830055e2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts @@ -7,7 +7,7 @@ import { RG_LAYER_OBJECT_GROUP_NAME, RG_LAYER_RECT_SHAPE_NAME, } from 'features/controlLayers/konva/naming'; -import type { EntityKonvaAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { KonvaEntityAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox'; import { createObjectGroup, @@ -52,7 +52,7 @@ const getRegion = ( manager: KonvaNodeManager, entity: RegionEntity, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -): EntityKonvaAdapter => { +): KonvaEntityAdapter => { const adapter = manager.get(entity.id); if (adapter) { return adapter; @@ -74,7 +74,7 @@ const getRegion = ( } const konvaObjectGroup = createObjectGroup(konvaLayer, RG_LAYER_OBJECT_GROUP_NAME); - return manager.add(entity.id, konvaLayer, konvaObjectGroup); + return manager.add(entity, konvaLayer, konvaObjectGroup); }; /** @@ -242,7 +242,7 @@ export const renderRegions = ( onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { // Destroy nonexistent layers - for (const adapter of manager.getAll()) { + for (const adapter of manager.getAll('regional_guidance')) { if (!entities.find((rg) => rg.id === adapter.id)) { manager.destroy(adapter.id); } 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 7c04a0a89b..a2533f78a9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -248,8 +248,10 @@ export const initializeRenderer = ( spaceKey = val; }; + const manager = new KonvaNodeManager(stage, getBbox, onBboxTransformed, $shift.get, $ctrl.get, $meta.get, $alt.get); + const cleanupListeners = setStageEventHandlers({ - stage, + manager, getToolState, setTool, setToolBuffer, @@ -284,10 +286,6 @@ export const initializeRenderer = ( // the entire state over when needed. const debouncedUpdateBboxes = debounce(updateBboxes, 300); - const regionMap = new KonvaNodeManager(stage); - const layerMap = new KonvaNodeManager(stage); - const controlAdapterMap = new KonvaNodeManager(stage); - const renderCanvas = () => { const { canvasV2 } = store.getState(); @@ -305,7 +303,7 @@ export const initializeRenderer = ( canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering layers'); - renderLayers(layerMap, canvasV2.layers, canvasV2.tool.selected, onPosChanged); + renderLayers(manager, canvasV2.layers, canvasV2.tool.selected, onPosChanged); } if ( @@ -316,7 +314,7 @@ export const initializeRenderer = ( ) { logIfDebugging('Rendering regions'); renderRegions( - regionMap, + manager, canvasV2.regions, canvasV2.settings.maskOpacity, canvasV2.tool.selected, @@ -327,27 +325,17 @@ export const initializeRenderer = ( if (isFirstRender || canvasV2.controlAdapters !== prevCanvasV2.controlAdapters) { logIfDebugging('Rendering control adapters'); - renderControlAdapters(controlAdapterMap, canvasV2.controlAdapters); + renderControlAdapters(manager, canvasV2.controlAdapters); } if (isFirstRender || canvasV2.document !== prevCanvasV2.document) { logIfDebugging('Rendering document bounds overlay'); - renderDocumentBoundsOverlay(stage, getDocument); + renderDocumentBoundsOverlay(manager, getDocument); } if (isFirstRender || canvasV2.bbox !== prevCanvasV2.bbox || canvasV2.tool.selected !== prevCanvasV2.tool.selected) { logIfDebugging('Rendering generation bbox'); - renderBboxPreview( - stage, - canvasV2.bbox, - canvasV2.tool.selected, - getBbox, - onBboxTransformed, - $shift.get, - $ctrl.get, - $meta.get, - $alt.get - ); + renderBboxPreview(manager, canvasV2.bbox, canvasV2.tool.selected); } if ( @@ -357,7 +345,7 @@ export const initializeRenderer = ( canvasV2.regions !== prevCanvasV2.regions ) { logIfDebugging('Updating entity bboxes'); - debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged); + // debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged); } if ( @@ -367,15 +355,7 @@ export const initializeRenderer = ( canvasV2.regions !== prevCanvasV2.regions ) { logIfDebugging('Arranging entities'); - arrangeEntities( - stage, - layerMap, - canvasV2.layers, - controlAdapterMap, - canvasV2.controlAdapters, - regionMap, - canvasV2.regions - ); + arrangeEntities(manager, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions); } prevCanvasV2 = canvasV2; @@ -399,8 +379,8 @@ export const initializeRenderer = ( height: stage.height(), scale: stage.scaleX(), }); - renderBackgroundLayer(stage); - renderDocumentBoundsOverlay(stage, getDocument); + renderBackgroundLayer(manager); + renderDocumentBoundsOverlay(manager, getDocument); }; const resizeObserver = new ResizeObserver(fitStageToContainer); @@ -414,7 +394,7 @@ export const initializeRenderer = ( const stageAttrs = fitDocumentToStage(stage, prevCanvasV2.document); // The HUD displays some of the stage attributes, so we need to update it here. $stageAttrs.set(stageAttrs); - scaleToolPreview(stage, getToolState()); + scaleToolPreview(manager, getToolState()); renderCanvas(); return () => {