diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/entityToKonvaMap.ts b/invokeai/frontend/web/src/features/controlLayers/konva/entityToKonvaMap.ts deleted file mode 100644 index 0bbe51575d..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/entityToKonvaMap.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { BrushLine, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types'; -import type Konva from 'konva'; - -export type BrushLineEntry = { - id: string; - type: BrushLine['type']; - konvaLine: Konva.Line; - konvaLineGroup: Konva.Group; -}; - -export type EraserLineEntry = { - id: string; - type: EraserLine['type']; - konvaLine: Konva.Line; - konvaLineGroup: Konva.Group; -}; - -export type RectShapeEntry = { - id: string; - type: RectShape['type']; - konvaRect: Konva.Rect; -}; - -export type ImageEntry = { - id: string; - type: ImageObject['type']; - konvaImageGroup: Konva.Group; - konvaPlaceholderGroup: Konva.Group; - konvaPlaceholderText: Konva.Text; - konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately - isLoading: boolean; - isError: boolean; -}; - -type Entry = BrushLineEntry | EraserLineEntry | RectShapeEntry | ImageEntry; - -export class EntityToKonvaMap { - stage: Konva.Stage; - mappings: Record; - - constructor(stage: Konva.Stage) { - this.stage = stage; - this.mappings = {}; - } - - addMapping(id: string, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group): EntityToKonvaMapping { - const mapping = new EntityToKonvaMapping(id, konvaLayer, konvaObjectGroup, this); - this.mappings[id] = mapping; - return mapping; - } - - getMapping(id: string): EntityToKonvaMapping | undefined { - return this.mappings[id]; - } - - getMappings(): EntityToKonvaMapping[] { - return Object.values(this.mappings); - } - - destroyMapping(id: string): void { - const mapping = this.getMapping(id); - if (!mapping) { - return; - } - mapping.konvaObjectGroup.destroy(); - delete this.mappings[id]; - } -} - -export class EntityToKonvaMapping { - id: string; - konvaLayer: Konva.Layer; - konvaObjectGroup: Konva.Group; - konvaNodeEntries: Record; - map: EntityToKonvaMap; - - constructor(id: string, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group, map: EntityToKonvaMap) { - this.id = id; - this.konvaLayer = konvaLayer; - this.konvaObjectGroup = konvaObjectGroup; - this.konvaNodeEntries = {}; - this.map = map; - } - - addEntry(entry: T): T { - this.konvaNodeEntries[entry.id] = entry; - return entry; - } - - getEntry(id: string): T | undefined { - return this.konvaNodeEntries[id] as T | undefined; - } - - getEntries(): T[] { - return Object.values(this.konvaNodeEntries) as T[]; - } - - destroyEntry(id: string): void { - const entry = this.getEntry(id); - if (!entry) { - return; - } - if (entry.type === 'brush_line' || entry.type === 'eraser_line') { - entry.konvaLineGroup.destroy(); - } else if (entry.type === 'rect_shape') { - entry.konvaRect.destroy(); - } else if (entry.type === 'image') { - entry.konvaImageGroup.destroy(); - } - delete this.konvaNodeEntries[id]; - } -} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts new file mode 100644 index 0000000000..f96f1f9893 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -0,0 +1,126 @@ +import type { BrushLine, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types'; +import type Konva from 'konva'; + +export type BrushLineObjectRecord = { + id: string; + type: BrushLine['type']; + konvaLine: Konva.Line; + konvaLineGroup: Konva.Group; +}; + +export type EraserLineObjectRecord = { + id: string; + type: EraserLine['type']; + konvaLine: Konva.Line; + konvaLineGroup: Konva.Group; +}; + +export type RectShapeObjectRecord = { + id: string; + type: RectShape['type']; + konvaRect: Konva.Rect; +}; + +export type ImageObjectRecord = { + id: string; + type: ImageObject['type']; + konvaImageGroup: Konva.Group; + konvaPlaceholderGroup: Konva.Group; + konvaPlaceholderRect: Konva.Rect; + konvaPlaceholderText: Konva.Text; + konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately + isLoading: boolean; + isError: boolean; +}; + +type ObjectRecord = BrushLineObjectRecord | EraserLineObjectRecord | RectShapeObjectRecord | ImageObjectRecord; + +export class KonvaNodeManager { + stage: Konva.Stage; + adapters: Map; + + constructor(stage: Konva.Stage) { + this.stage = stage; + this.adapters = new Map(); + } + + add(id: string, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group): EntityKonvaAdapter { + const adapter = new EntityKonvaAdapter(id, konvaLayer, konvaObjectGroup, this); + this.adapters.set(id, adapter); + return adapter; + } + + get(id: string): EntityKonvaAdapter | undefined { + return this.adapters.get(id); + } + + getAll(): EntityKonvaAdapter[] { + return Array.from(this.adapters.values()); + } + + destroy(id: string): boolean { + const adapter = this.get(id); + if (!adapter) { + return false; + } + adapter.konvaLayer.destroy(); + return this.adapters.delete(id); + } +} + +export class EntityKonvaAdapter { + id: string; + 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; + this.konvaLayer = konvaLayer; + this.konvaObjectGroup = konvaObjectGroup; + this.objectRecords = new Map(); + this.manager = manager; + this.konvaLayer.add(this.konvaObjectGroup); + this.manager.stage.add(this.konvaLayer); + } + + add(objectRecord: T): T { + this.objectRecords.set(objectRecord.id, objectRecord); + if (objectRecord.type === 'brush_line' || objectRecord.type === 'eraser_line') { + objectRecord.konvaLineGroup.add(objectRecord.konvaLine); + this.konvaObjectGroup.add(objectRecord.konvaLineGroup); + } else if (objectRecord.type === 'rect_shape') { + this.konvaObjectGroup.add(objectRecord.konvaRect); + } else if (objectRecord.type === 'image') { + objectRecord.konvaPlaceholderGroup.add(objectRecord.konvaPlaceholderRect); + objectRecord.konvaPlaceholderGroup.add(objectRecord.konvaPlaceholderText); + objectRecord.konvaImageGroup.add(objectRecord.konvaPlaceholderGroup); + this.konvaObjectGroup.add(objectRecord.konvaImageGroup); + } + return objectRecord; + } + + get(id: string): T | undefined { + return this.objectRecords.get(id) as T | undefined; + } + + getAll(): T[] { + return Array.from(this.objectRecords.values()) as T[]; + } + + destroy(id: string): boolean { + const record = this.get(id); + if (!record) { + return false; + } + if (record.type === 'brush_line' || record.type === 'eraser_line') { + record.konvaLineGroup.destroy(); + } else if (record.type === 'rect_shape') { + record.konvaRect.destroy(); + } else if (record.type === 'image') { + record.konvaImageGroup.destroy(); + } + return this.objectRecords.delete(id); + } +} 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 02644c1cc4..95bc6d005c 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,27 @@ -import type { EntityToKonvaMap } from 'features/controlLayers/konva/entityToKonvaMap'; 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, - layerMap: EntityToKonvaMap, + layerManager: KonvaNodeManager, layers: LayerEntity[], - controlAdapterMap: EntityToKonvaMap, + controlAdapterManager: KonvaNodeManager, controlAdapters: ControlAdapterEntity[], - regionMap: EntityToKonvaMap, + regionManager: KonvaNodeManager, regions: RegionEntity[] ): void => { let zIndex = 0; stage.findOne(`#${BACKGROUND_LAYER_ID}`)?.zIndex(++zIndex); for (const layer of layers) { - layerMap.getMapping(layer.id)?.konvaLayer.zIndex(++zIndex); + layerManager.get(layer.id)?.konvaLayer.zIndex(++zIndex); } for (const ca of controlAdapters) { - controlAdapterMap.getMapping(ca.id)?.konvaLayer.zIndex(++zIndex); + controlAdapterManager.get(ca.id)?.konvaLayer.zIndex(++zIndex); } for (const rg of regions) { - regionMap.getMapping(rg.id)?.konvaLayer.zIndex(++zIndex); + regionManager.get(rg.id)?.konvaLayer.zIndex(++zIndex); } stage.findOne(`#${PREVIEW_LAYER_ID}`)?.zIndex(++zIndex); }; 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 c466db06da..9b9d624a2b 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 type { EntityToKonvaMap, EntityToKonvaMapping, ImageEntry } from 'features/controlLayers/konva/entityToKonvaMap'; 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 { createImageObjectGroup, createObjectGroup, @@ -21,10 +21,10 @@ import { assert } from 'tsafe'; * @param stage The konva stage * @param entity The control adapter layer state */ -const getControlAdapter = (map: EntityToKonvaMap, entity: ControlAdapterEntity): EntityToKonvaMapping => { - let mapping = map.getMapping(entity.id); - if (mapping) { - return mapping; +const getControlAdapter = (manager: KonvaNodeManager, entity: ControlAdapterEntity): EntityKonvaAdapter => { + const adapter = manager.get(entity.id); + if (adapter) { + return adapter; } const konvaLayer = new Konva.Layer({ id: entity.id, @@ -33,9 +33,9 @@ const getControlAdapter = (map: EntityToKonvaMap, entity: ControlAdapterEntity): listening: false, }); const konvaObjectGroup = createObjectGroup(konvaLayer, CA_LAYER_OBJECT_GROUP_NAME); - map.stage.add(konvaLayer); - mapping = map.addMapping(entity.id, konvaLayer, konvaObjectGroup); - return mapping; + konvaLayer.add(konvaObjectGroup); + manager.stage.add(konvaLayer); + return manager.add(entity.id, konvaLayer, konvaObjectGroup); }; /** @@ -45,26 +45,26 @@ const getControlAdapter = (map: EntityToKonvaMap, entity: ControlAdapterEntity): * @param entity The control adapter layer state * @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source */ -export const renderControlAdapter = async (map: EntityToKonvaMap, entity: ControlAdapterEntity): Promise => { - const mapping = getControlAdapter(map, entity); +export const renderControlAdapter = async (manager: KonvaNodeManager, entity: ControlAdapterEntity): Promise => { + const adapter = getControlAdapter(manager, entity); const imageObject = entity.processedImageObject ?? entity.imageObject; if (!imageObject) { // The user has deleted/reset the image - mapping.getEntries().forEach((entry) => { - mapping.destroyEntry(entry.id); + adapter.getAll().forEach((entry) => { + adapter.destroy(entry.id); }); return; } - let entry = mapping.getEntries()[0]; + let entry = adapter.getAll()[0]; const opacity = entity.opacity; const visible = entity.isEnabled; const filters = entity.filter === 'LightnessToAlphaFilter' ? [LightnessToAlphaFilter] : []; if (!entry) { entry = await createImageObjectGroup({ - mapping, + adapter: adapter, obj: imageObject, name: CA_LAYER_IMAGE_NAME, onLoad: (konvaImage) => { @@ -83,7 +83,7 @@ export const renderControlAdapter = async (map: EntityToKonvaMap, entity: Contro assert(imageSource instanceof HTMLImageElement, `Image source must be an HTMLImageElement`); if (imageSource.id !== imageObject.image.name) { updateImageSource({ - entry, + objectRecord: entry, image: imageObject.image, onLoad: (konvaImage) => { konvaImage.filters(filters); @@ -103,14 +103,14 @@ export const renderControlAdapter = async (map: EntityToKonvaMap, entity: Contro } }; -export const renderControlAdapters = (map: EntityToKonvaMap, entities: ControlAdapterEntity[]): void => { +export const renderControlAdapters = (manager: KonvaNodeManager, entities: ControlAdapterEntity[]): void => { // Destroy nonexistent layers - for (const mapping of map.getMappings()) { - if (!entities.find((ca) => ca.id === mapping.id)) { - map.destroyMapping(mapping.id); + for (const adapters of manager.getAll()) { + if (!entities.find((ca) => ca.id === adapters.id)) { + manager.destroy(adapters.id); } } - for (const ca of entities) { - renderControlAdapter(map, ca); + for (const entity of entities) { + renderControlAdapter(manager, entity); } }; 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 6c128953fd..a3c144f744 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -1,4 +1,3 @@ -import type { EntityToKonvaMap, EntityToKonvaMapping } from 'features/controlLayers/konva/entityToKonvaMap'; import { RASTER_LAYER_BRUSH_LINE_NAME, RASTER_LAYER_ERASER_LINE_NAME, @@ -7,6 +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 { createImageObjectGroup, createObjectGroup, @@ -29,13 +29,13 @@ import Konva from 'konva'; * @param onPosChanged Callback for when the layer's position changes */ const getLayer = ( - map: EntityToKonvaMap, + map: KonvaNodeManager, entity: LayerEntity, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -): EntityToKonvaMapping => { - let mapping = map.getMapping(entity.id); - if (mapping) { - return mapping; +): EntityKonvaAdapter => { + const adapter = map.get(entity.id); + if (adapter) { + return adapter; } // This layer hasn't been added to the konva state yet const konvaLayer = new Konva.Layer({ @@ -54,9 +54,7 @@ const getLayer = ( } const konvaObjectGroup = createObjectGroup(konvaLayer, RASTER_LAYER_OBJECT_GROUP_NAME); - map.stage.add(konvaLayer); - mapping = map.addMapping(entity.id, konvaLayer, konvaObjectGroup); - return mapping; + return map.add(entity.id, konvaLayer, konvaObjectGroup); }; /** @@ -67,15 +65,15 @@ const getLayer = ( * @param onPosChanged Callback for when the layer's position changes */ export const renderLayer = async ( - map: EntityToKonvaMap, + manager: KonvaNodeManager, entity: LayerEntity, tool: Tool, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ) => { - const mapping = getLayer(map, entity, onPosChanged); + const adapter = getLayer(manager, entity, onPosChanged); // Update the layer's position and listening state - mapping.konvaLayer.setAttrs({ + adapter.konvaLayer.setAttrs({ listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events x: Math.floor(entity.x), y: Math.floor(entity.y), @@ -83,35 +81,35 @@ export const renderLayer = async ( const objectIds = entity.objects.map(mapId); // Destroy any objects that are no longer in state - for (const entry of mapping.getEntries()) { - if (!objectIds.includes(entry.id)) { - mapping.destroyEntry(entry.id); + for (const objectRecord of adapter.getAll()) { + if (!objectIds.includes(objectRecord.id)) { + adapter.destroy(objectRecord.id); } } for (const obj of entity.objects) { if (obj.type === 'brush_line') { - const entry = getBrushLine(mapping, obj, RASTER_LAYER_BRUSH_LINE_NAME); + const objectRecord = getBrushLine(adapter, obj, RASTER_LAYER_BRUSH_LINE_NAME); // Only update the points if they have changed. - if (entry.konvaLine.points().length !== obj.points.length) { - entry.konvaLine.points(obj.points); + if (objectRecord.konvaLine.points().length !== obj.points.length) { + objectRecord.konvaLine.points(obj.points); } } else if (obj.type === 'eraser_line') { - const entry = getEraserLine(mapping, obj, RASTER_LAYER_ERASER_LINE_NAME); + const objectRecord = getEraserLine(adapter, obj, RASTER_LAYER_ERASER_LINE_NAME); // Only update the points if they have changed. - if (entry.konvaLine.points().length !== obj.points.length) { - entry.konvaLine.points(obj.points); + if (objectRecord.konvaLine.points().length !== obj.points.length) { + objectRecord.konvaLine.points(obj.points); } } else if (obj.type === 'rect_shape') { - getRectShape(mapping, obj, RASTER_LAYER_RECT_SHAPE_NAME); + getRectShape(adapter, obj, RASTER_LAYER_RECT_SHAPE_NAME); } else if (obj.type === 'image') { - createImageObjectGroup({ mapping, obj, name: RASTER_LAYER_IMAGE_NAME }); + createImageObjectGroup({ adapter, obj, name: RASTER_LAYER_IMAGE_NAME }); } } // Only update layer visibility if it has changed. - if (mapping.konvaLayer.visible() !== entity.isEnabled) { - mapping.konvaLayer.visible(entity.isEnabled); + if (adapter.konvaLayer.visible() !== entity.isEnabled) { + adapter.konvaLayer.visible(entity.isEnabled); } // const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); @@ -132,22 +130,22 @@ export const renderLayer = async ( // bboxRect.visible(false); // } - mapping.konvaObjectGroup.opacity(entity.opacity); + adapter.konvaObjectGroup.opacity(entity.opacity); }; export const renderLayers = ( - map: EntityToKonvaMap, + manager: KonvaNodeManager, entities: LayerEntity[], tool: Tool, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { // Destroy nonexistent layers - for (const mapping of map.getMappings()) { - if (!entities.find((l) => l.id === mapping.id)) { - map.destroyMapping(mapping.id); + for (const adapter of manager.getAll()) { + if (!entities.find((l) => l.id === adapter.id)) { + manager.destroy(adapter.id); } } - for (const layer of entities) { - renderLayer(map, layer, tool, onPosChanged); + for (const entity of entities) { + renderLayer(manager, entity, tool, onPosChanged); } }; 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 4054bc8ec7..64c39b21d5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -1,11 +1,4 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; -import type { - BrushLineEntry, - EntityToKonvaMapping, - EraserLineEntry, - ImageEntry, - RectShapeEntry, -} from 'features/controlLayers/konva/entityToKonvaMap'; import { getLayerBboxId, getObjectGroupId, @@ -13,6 +6,13 @@ import { LAYER_BBOX_NAME, PREVIEW_GENERATION_BBOX_DUMMY_RECT, } from 'features/controlLayers/konva/naming'; +import type { + BrushLineObjectRecord, + EntityKonvaAdapter, + EraserLineObjectRecord, + ImageObjectRecord, + RectShapeObjectRecord, +} from 'features/controlLayers/konva/nodeManager'; import type { BrushLine, CanvasEntity, @@ -39,32 +39,33 @@ import { v4 as uuidv4 } from 'uuid'; * @param layerObjectGroup The konva layer's object group to add the line to * @param name The konva name for the line */ -export const getBrushLine = (mapping: EntityToKonvaMapping, brushLine: BrushLine, name: string): BrushLineEntry => { - let entry = mapping.getEntry(brushLine.id); - if (entry) { - return entry; +export const getBrushLine = ( + adapter: EntityKonvaAdapter, + brushLine: BrushLine, + name: string +): BrushLineObjectRecord => { + const objectRecord = adapter.get(brushLine.id); + if (objectRecord) { + return objectRecord; } - + const { id, strokeWidth, clip, color } = brushLine; const konvaLineGroup = new Konva.Group({ - clip: brushLine.clip, + clip, listening: false, }); const konvaLine = new Konva.Line({ - id: brushLine.id, + id, name, - strokeWidth: brushLine.strokeWidth, + strokeWidth, tension: 0, lineCap: 'round', lineJoin: 'round', shadowForStrokeEnabled: false, globalCompositeOperation: 'source-over', listening: false, - stroke: rgbaColorToString(brushLine.color), + stroke: rgbaColorToString(color), }); - konvaLineGroup.add(konvaLine); - mapping.konvaObjectGroup.add(konvaLineGroup); - entry = mapping.addEntry({ id: brushLine.id, type: 'brush_line', konvaLine, konvaLineGroup }); - return entry; + return adapter.add({ id, type: 'brush_line', konvaLine, konvaLineGroup }); }; /** @@ -73,20 +74,25 @@ export const getBrushLine = (mapping: EntityToKonvaMapping, brushLine: BrushLine * @param layerObjectGroup The konva layer's object group to add the line to * @param name The konva name for the line */ -export const getEraserLine = (mapping: EntityToKonvaMapping, eraserLine: EraserLine, name: string): EraserLineEntry => { - let entry = mapping.getEntry(eraserLine.id); - if (entry) { - return entry; +export const getEraserLine = ( + adapter: EntityKonvaAdapter, + eraserLine: EraserLine, + name: string +): EraserLineObjectRecord => { + const objectRecord = adapter.get(eraserLine.id); + if (objectRecord) { + return objectRecord; } + const { id, strokeWidth, clip } = eraserLine; const konvaLineGroup = new Konva.Group({ - clip: eraserLine.clip, + clip, listening: false, }); const konvaLine = new Konva.Line({ - id: eraserLine.id, + id, name, - strokeWidth: eraserLine.strokeWidth, + strokeWidth, tension: 0, lineCap: 'round', lineJoin: 'round', @@ -94,12 +100,8 @@ export const getEraserLine = (mapping: EntityToKonvaMapping, eraserLine: EraserL globalCompositeOperation: 'destination-out', listening: false, stroke: rgbaColorToString(DEFAULT_RGBA_COLOR), - clip: eraserLine.clip, }); - konvaLineGroup.add(konvaLine); - mapping.konvaObjectGroup.add(konvaLineGroup); - entry = mapping.addEntry({ id: eraserLine.id, type: 'eraser_line', konvaLine, konvaLineGroup }); - return entry; + return adapter.add({ id, type: 'eraser_line', konvaLine, konvaLineGroup }); }; /** @@ -108,87 +110,89 @@ export const getEraserLine = (mapping: EntityToKonvaMapping, eraserLine: EraserL * @param layerObjectGroup The konva layer's object group to add the rect to * @param name The konva name for the rect */ -export const getRectShape = (mapping: EntityToKonvaMapping, rectShape: RectShape, name: string): RectShapeEntry => { - let entry = mapping.getEntry(rectShape.id); - if (entry) { - return entry; +export const getRectShape = ( + adapter: EntityKonvaAdapter, + rectShape: RectShape, + name: string +): RectShapeObjectRecord => { + const objectRecord = adapter.get(rectShape.id); + if (objectRecord) { + return objectRecord; } + const { id, x, y, width, height } = rectShape; const konvaRect = new Konva.Rect({ - id: rectShape.id, - key: rectShape.id, + id, name, - x: rectShape.x, - y: rectShape.y, - width: rectShape.width, - height: rectShape.height, + x, + y, + width, + height, listening: false, fill: rgbaColorToString(rectShape.color), }); - mapping.konvaObjectGroup.add(konvaRect); - entry = mapping.addEntry({ id: rectShape.id, type: 'rect_shape', konvaRect }); - return entry; + return adapter.add({ id: rectShape.id, type: 'rect_shape', konvaRect }); }; export const updateImageSource = async (arg: { - entry: ImageEntry; + objectRecord: ImageObjectRecord; image: ImageWithDims; getImageDTO?: (imageName: string) => Promise; onLoading?: () => void; onLoad?: (konvaImage: Konva.Image) => void; onError?: () => void; }) => { - const { entry, image, getImageDTO = defaultGetImageDTO, onLoading, onLoad, onError } = arg; + const { objectRecord, image, getImageDTO = defaultGetImageDTO, onLoading, onLoad, onError } = arg; try { - entry.isLoading = true; - if (!entry.konvaImage) { - entry.konvaPlaceholderGroup.visible(true); - entry.konvaPlaceholderText.text(t('common.loadingImage', 'Loading Image')); + objectRecord.isLoading = true; + if (!objectRecord.konvaImage) { + objectRecord.konvaPlaceholderGroup.visible(true); + objectRecord.konvaPlaceholderText.text(t('common.loadingImage', 'Loading Image')); } onLoading?.(); const imageDTO = await getImageDTO(image.name); if (!imageDTO) { - entry.isLoading = false; - entry.isError = true; - entry.konvaPlaceholderGroup.visible(true); - entry.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); + objectRecord.isLoading = false; + objectRecord.isError = true; + objectRecord.konvaPlaceholderGroup.visible(true); + objectRecord.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); onError?.(); return; } const imageEl = new Image(); imageEl.onload = () => { - if (entry.konvaImage) { - entry.konvaImage.setAttrs({ + if (objectRecord.konvaImage) { + objectRecord.konvaImage.setAttrs({ image: imageEl, }); } else { - entry.konvaImage = new Konva.Image({ - id: entry.id, + objectRecord.konvaImage = new Konva.Image({ + id: objectRecord.id, listening: false, image: imageEl, }); - entry.konvaImageGroup.add(entry.konvaImage); + objectRecord.konvaImageGroup.add(objectRecord.konvaImage); } - entry.isLoading = false; - entry.isError = false; - entry.konvaPlaceholderGroup.visible(false); - onLoad?.(entry.konvaImage); + objectRecord.isLoading = false; + objectRecord.isError = false; + objectRecord.konvaPlaceholderGroup.visible(false); + onLoad?.(objectRecord.konvaImage); }; imageEl.onerror = () => { - entry.isLoading = false; - entry.isError = true; - entry.konvaPlaceholderGroup.visible(true); - entry.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); + objectRecord.isLoading = false; + objectRecord.isError = true; + objectRecord.konvaPlaceholderGroup.visible(true); + objectRecord.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); onError?.(); }; imageEl.id = image.name; imageEl.src = imageDTO.image_url; } catch { - entry.isLoading = false; - entry.isError = true; - entry.konvaPlaceholderGroup.visible(true); - entry.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); + objectRecord.isLoading = false; + objectRecord.isError = true; + objectRecord.konvaPlaceholderGroup.visible(true); + objectRecord.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); onError?.(); } }; @@ -199,18 +203,18 @@ 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: { - mapping: EntityToKonvaMapping; + adapter: EntityKonvaAdapter; obj: ImageObject; name: string; getImageDTO?: (imageName: string) => Promise; onLoad?: (konvaImage: Konva.Image) => void; onLoading?: () => void; onError?: () => void; -}): ImageEntry => { - const { mapping, obj, name, getImageDTO = defaultGetImageDTO, onLoad, onLoading, onError } = arg; - let entry = mapping.getEntry(obj.id); - if (entry) { - return entry; +}): ImageObjectRecord => { + const { adapter, obj, name, getImageDTO = defaultGetImageDTO, onLoad, onLoading, onError } = arg; + let objectRecord = adapter.get(obj.id); + if (objectRecord) { + return objectRecord; } const { id, image } = obj; const { width, height } = obj; @@ -234,23 +238,19 @@ export const createImageObjectGroup = (arg: { text: t('common.loadingImage', 'Loading Image'), listening: false, }); - konvaPlaceholderGroup.add(konvaPlaceholderRect); - konvaPlaceholderGroup.add(konvaPlaceholderText); - konvaImageGroup.add(konvaPlaceholderGroup); - mapping.konvaObjectGroup.add(konvaImageGroup); - - entry = mapping.addEntry({ + objectRecord = adapter.add({ id, type: 'image', konvaImageGroup, konvaPlaceholderGroup, + konvaPlaceholderRect, konvaPlaceholderText, konvaImage: null, isLoading: false, isError: false, }); - updateImageSource({ entry, image, getImageDTO, onLoad, onLoading, onError }); - return entry; + updateImageSource({ objectRecord, image, getImageDTO, onLoad, onLoading, onError }); + return objectRecord; }; /** 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 5119b2356d..7e5f4b748a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts @@ -1,5 +1,4 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import type { EntityToKonvaMap, EntityToKonvaMapping } from 'features/controlLayers/konva/entityToKonvaMap'; import { COMPOSITING_RECT_NAME, RG_LAYER_BRUSH_LINE_NAME, @@ -8,6 +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 { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox'; import { createObjectGroup, @@ -49,13 +49,13 @@ const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { * @param onLayerPosChanged Callback for when the layer's position changes */ const getRegion = ( - map: EntityToKonvaMap, + manager: KonvaNodeManager, entity: RegionEntity, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -): EntityToKonvaMapping => { - let mapping = map.getMapping(entity.id); - if (mapping) { - return mapping; +): EntityKonvaAdapter => { + const adapter = manager.get(entity.id); + if (adapter) { + return adapter; } // This layer hasn't been added to the konva state yet const konvaLayer = new Konva.Layer({ @@ -74,9 +74,7 @@ const getRegion = ( } const konvaObjectGroup = createObjectGroup(konvaLayer, RG_LAYER_OBJECT_GROUP_NAME); - map.stage.add(konvaLayer); - mapping = map.addMapping(entity.id, konvaLayer, konvaObjectGroup); - return mapping; + return manager.add(entity.id, konvaLayer, konvaObjectGroup); }; /** @@ -88,17 +86,17 @@ const getRegion = ( * @param onPosChanged Callback for when the layer's position changes */ export const renderRegion = ( - map: EntityToKonvaMap, + manager: KonvaNodeManager, entity: RegionEntity, globalMaskLayerOpacity: number, tool: Tool, selectedEntityIdentifier: CanvasEntityIdentifier | null, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { - const mapping = getRegion(map, entity, onPosChanged); + const adapter = getRegion(manager, entity, onPosChanged); // Update the layer's position and listening state - mapping.konvaLayer.setAttrs({ + adapter.konvaLayer.setAttrs({ listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events x: Math.floor(entity.x), y: Math.floor(entity.y), @@ -112,67 +110,67 @@ export const renderRegion = ( const objectIds = entity.objects.map(mapId); // Destroy any objects that are no longer in state - for (const entry of mapping.getEntries()) { - if (!objectIds.includes(entry.id)) { - mapping.destroyEntry(entry.id); + for (const objectRecord of adapter.getAll()) { + if (!objectIds.includes(objectRecord.id)) { + adapter.destroy(objectRecord.id); groupNeedsCache = true; } } for (const obj of entity.objects) { if (obj.type === 'brush_line') { - const entry = getBrushLine(mapping, obj, RG_LAYER_BRUSH_LINE_NAME); + const objectRecord = getBrushLine(adapter, obj, RG_LAYER_BRUSH_LINE_NAME); // Only update the points if they have changed. The point values are never mutated, they are only added to the // array, so checking the length is sufficient to determine if we need to re-cache. - if (entry.konvaLine.points().length !== obj.points.length) { - entry.konvaLine.points(obj.points); + if (objectRecord.konvaLine.points().length !== obj.points.length) { + objectRecord.konvaLine.points(obj.points); groupNeedsCache = true; } // Only update the color if it has changed. - if (entry.konvaLine.stroke() !== rgbColor) { - entry.konvaLine.stroke(rgbColor); + if (objectRecord.konvaLine.stroke() !== rgbColor) { + objectRecord.konvaLine.stroke(rgbColor); groupNeedsCache = true; } } else if (obj.type === 'eraser_line') { - const entry = getEraserLine(mapping, obj, RG_LAYER_ERASER_LINE_NAME); + const objectRecord = getEraserLine(adapter, obj, RG_LAYER_ERASER_LINE_NAME); // Only update the points if they have changed. The point values are never mutated, they are only added to the // array, so checking the length is sufficient to determine if we need to re-cache. - if (entry.konvaLine.points().length !== obj.points.length) { - entry.konvaLine.points(obj.points); + if (objectRecord.konvaLine.points().length !== obj.points.length) { + objectRecord.konvaLine.points(obj.points); groupNeedsCache = true; } // Only update the color if it has changed. - if (entry.konvaLine.stroke() !== rgbColor) { - entry.konvaLine.stroke(rgbColor); + if (objectRecord.konvaLine.stroke() !== rgbColor) { + objectRecord.konvaLine.stroke(rgbColor); groupNeedsCache = true; } } else if (obj.type === 'rect_shape') { - const entry = getRectShape(mapping, obj, RG_LAYER_RECT_SHAPE_NAME); + const objectRecord = getRectShape(adapter, obj, RG_LAYER_RECT_SHAPE_NAME); // Only update the color if it has changed. - if (entry.konvaRect.fill() !== rgbColor) { - entry.konvaRect.fill(rgbColor); + if (objectRecord.konvaRect.fill() !== rgbColor) { + objectRecord.konvaRect.fill(rgbColor); groupNeedsCache = true; } } } // Only update layer visibility if it has changed. - if (mapping.konvaLayer.visible() !== entity.isEnabled) { - mapping.konvaLayer.visible(entity.isEnabled); + if (adapter.konvaLayer.visible() !== entity.isEnabled) { + adapter.konvaLayer.visible(entity.isEnabled); groupNeedsCache = true; } - if (mapping.konvaObjectGroup.getChildren().length === 0) { + if (adapter.konvaObjectGroup.getChildren().length === 0) { // No objects - clear the cache to reset the previous pixel data - mapping.konvaObjectGroup.clearCache(); + adapter.konvaObjectGroup.clearCache(); return; } const compositingRect = - mapping.konvaLayer.findOne(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(mapping.konvaLayer); + adapter.konvaLayer.findOne(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(adapter.konvaLayer); const isSelected = selectedEntityIdentifier?.id === entity.id; /** @@ -188,32 +186,32 @@ export const renderRegion = ( */ if (isSelected && tool !== 'move') { // We must clear the cache first so Konva will re-draw the group with the new compositing rect - if (mapping.konvaObjectGroup.isCached()) { - mapping.konvaObjectGroup.clearCache(); + if (adapter.konvaObjectGroup.isCached()) { + adapter.konvaObjectGroup.clearCache(); } // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - mapping.konvaObjectGroup.opacity(1); + adapter.konvaObjectGroup.opacity(1); compositingRect.setAttrs({ // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already - ...(!entity.bboxNeedsUpdate && entity.bbox ? entity.bbox : getLayerBboxFast(mapping.konvaLayer)), + ...(!entity.bboxNeedsUpdate && entity.bbox ? entity.bbox : getLayerBboxFast(adapter.konvaLayer)), fill: rgbColor, opacity: globalMaskLayerOpacity, // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) globalCompositeOperation: 'source-in', visible: true, // This rect must always be on top of all other shapes - zIndex: mapping.konvaObjectGroup.getChildren().length, + zIndex: adapter.konvaObjectGroup.getChildren().length, }); } else { // The compositing rect should only be shown when the layer is selected. compositingRect.visible(false); // Cache only if needed - or if we are on this code path and _don't_ have a cache - if (groupNeedsCache || !mapping.konvaObjectGroup.isCached()) { - mapping.konvaObjectGroup.cache(); + if (groupNeedsCache || !adapter.konvaObjectGroup.isCached()) { + adapter.konvaObjectGroup.cache(); } // Updating group opacity does not require re-caching - mapping.konvaObjectGroup.opacity(globalMaskLayerOpacity); + adapter.konvaObjectGroup.opacity(globalMaskLayerOpacity); } // const bboxRect = @@ -236,7 +234,7 @@ export const renderRegion = ( }; export const renderRegions = ( - map: EntityToKonvaMap, + manager: KonvaNodeManager, entities: RegionEntity[], maskOpacity: number, tool: Tool, @@ -244,12 +242,12 @@ export const renderRegions = ( onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { // Destroy nonexistent layers - for (const mapping of map.getMappings()) { - if (!entities.find((rg) => rg.id === mapping.id)) { - map.destroyMapping(mapping.id); + for (const adapter of manager.getAll()) { + if (!entities.find((rg) => rg.id === adapter.id)) { + manager.destroy(adapter.id); } } - for (const rg of entities) { - renderRegion(map, rg, maskOpacity, tool, selectedEntityIdentifier, onPosChanged); + for (const entity of entities) { + renderRegion(manager, entity, maskOpacity, tool, selectedEntityIdentifier, onPosChanged); } }; 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 a458dbec54..4411a37394 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -3,7 +3,7 @@ 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 { EntityToKonvaMap } from 'features/controlLayers/konva/entityToKonvaMap'; +import { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { arrangeEntities } from 'features/controlLayers/konva/renderers/arrange'; import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; @@ -282,9 +282,9 @@ export const initializeRenderer = ( // the entire state over when needed. const debouncedUpdateBboxes = debounce(updateBboxes, 300); - const regionMap = new EntityToKonvaMap(stage); - const layerMap = new EntityToKonvaMap(stage); - const controlAdapterMap = new EntityToKonvaMap(stage); + const regionMap = new KonvaNodeManager(stage); + const layerMap = new KonvaNodeManager(stage); + const controlAdapterMap = new KonvaNodeManager(stage); const renderCanvas = () => { const { canvasV2 } = store.getState();