From b6d845a4d0e8c0fb2bf8a3f8b1089b683c70a4cd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 2 Aug 2024 19:21:20 +1000 Subject: [PATCH] feat(ui): document & clean up object renderer --- .../controlLayers/konva/CanvasBrushLine.ts | 6 +- .../controlLayers/konva/CanvasEraserLine.ts | 6 +- .../controlLayers/konva/CanvasImage.ts | 6 +- .../controlLayers/konva/CanvasLayer.ts | 1 - .../konva/CanvasObjectRenderer.ts | 92 +++++++++++++------ .../controlLayers/konva/CanvasRect.ts | 4 +- 6 files changed, 70 insertions(+), 45 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index d842efff11..1b53344cca 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -23,7 +23,6 @@ export class CanvasBrushLineRenderer { group: Konva.Group; line: Konva.Line; }; - isFirstRender: boolean = false; constructor(state: CanvasBrushLineState, parent: CanvasObjectRenderer) { const { id, strokeWidth, clip, color, points } = state; @@ -60,10 +59,8 @@ export class CanvasBrushLineRenderer { this.state = state; } - update(state: CanvasBrushLineState, force = this.isFirstRender): boolean { + update(state: CanvasBrushLineState, force = false): boolean { if (force || this.state !== state) { - this.isFirstRender = false; - this.log.trace({ state }, 'Updating brush line'); const { points, color, clip, strokeWidth } = state; this.konva.line.setAttrs({ @@ -95,7 +92,6 @@ export class CanvasBrushLineRenderer { id: this.id, type: CanvasBrushLineRenderer.TYPE, parent: this.parent.id, - isFirstRender: this.isFirstRender, state: deepClone(this.state), }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index 1f4679807d..26540ae974 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -18,7 +18,6 @@ export class CanvasEraserLineRenderer { log: Logger; getLoggingContext: GetLoggingContext; - isFirstRender: boolean = false; state: CanvasEraserLineState; konva: { group: Konva.Group; @@ -59,10 +58,8 @@ export class CanvasEraserLineRenderer { this.state = state; } - update(state: CanvasEraserLineState, force = this.isFirstRender): boolean { + update(state: CanvasEraserLineState, force = false): boolean { if (force || this.state !== state) { - this.isFirstRender = false; - this.log.trace({ state }, 'Updating eraser line'); const { points, clip, strokeWidth } = state; this.konva.line.setAttrs({ @@ -93,7 +90,6 @@ export class CanvasEraserLineRenderer { id: this.id, type: CanvasEraserLineRenderer.TYPE, parent: this.parent.id, - isFirstRender: this.isFirstRender, state: deepClone(this.state), }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index 77ec5a2a68..1e574217cc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -32,7 +32,6 @@ export class CanvasImageRenderer { imageName: string | null; isLoading: boolean; isError: boolean; - isFirstRender: boolean = true; constructor(state: CanvasImageState, parent: CanvasObjectRenderer) { const { id, width, height, x, y } = state; @@ -138,10 +137,8 @@ export class CanvasImageRenderer { } }; - update = async (state: CanvasImageState, force = this.isFirstRender): Promise => { + update = async (state: CanvasImageState, force = false): Promise => { if (force || this.state !== state) { - this.isFirstRender = false; - this.log.trace({ state }, 'Updating image'); const { width, height, x, y, image, filters } = state; @@ -183,7 +180,6 @@ export class CanvasImageRenderer { imageName: this.imageName, isLoading: this.isLoading, isError: this.isError, - isFirstRender: this.isFirstRender, state: deepClone(this.state), }; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 684f7b2d6e..14acffcf09 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -259,7 +259,6 @@ export class CanvasLayer { const { dispatch } = getStore(); const imageObject = imageDTOToImageObject(imageDTO); await this.renderer.renderObject(imageObject, true); - this.renderer.hideAll([imageObject.id]); this.resetScale(); dispatch(layerRasterized({ id: this.id, imageObject, position: { x: Math.round(rect.x), y: Math.round(rect.y) } })); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index 559f21cd90..ab2ae03844 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -16,12 +16,17 @@ import type { import type { Logger } from 'roarr'; import { assert } from 'tsafe'; +/** + * Union of all object renderers. + */ type AnyObjectRenderer = CanvasBrushLineRenderer | CanvasEraserLineRenderer | CanvasRectRenderer | CanvasImageRenderer; +/** + * Union of all object states. + */ type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImageState | CanvasRectState; export class CanvasObjectRenderer { static TYPE = 'object_renderer'; - static OBJECT_GROUP_NAME = `${CanvasObjectRenderer.TYPE}_group`; id: string; parent: CanvasLayer; @@ -29,9 +34,16 @@ export class CanvasObjectRenderer { log: Logger; getLoggingContext: (extra?: JSONObject) => JSONObject; - isFirstRender: boolean = true; - isRendering: boolean = false; + /** + * A buffer object state that is rendered separately from the other objects. This is used for objects that are being + * drawn in real-time, such as brush lines. The buffer object state only exists in this renderer and is not part of + * the application state until it is committed. + */ buffer: AnyObjectState | null = null; + + /** + * A map of object renderers, keyed by their ID. + */ renderers: Map = new Map(); constructor(parent: CanvasLayer) { @@ -43,8 +55,12 @@ export class CanvasObjectRenderer { this.log.trace('Creating object renderer'); } + /** + * Renders the given objects. + * @param objectStates The objects to render. + * @returns A promise that resolves to a boolean, indicating if any of the objects were rendered. + */ render = async (objectStates: AnyObjectState[]): Promise => { - this.isRendering = true; let didRender = false; const objectIds = objectStates.map((objectState) => objectState.id); @@ -64,17 +80,26 @@ export class CanvasObjectRenderer { didRender = (await this.renderObject(this.buffer)) || didRender; } - this.isRendering = false; - this.isFirstRender = false; - return didRender; }; - renderObject = async (objectState: AnyObjectState, force?: boolean): Promise => { + /** + * Renders the given object. If the object renderer does not exist, it will be created and its Konva group added to the + * parent entity's object group. + * @param objectState The object's state. + * @param force Whether to force the object to render, even if it has not changed. If omitted, the object renderer + * will only render if the object state has changed. The exception is the first render, where the object will always + * be rendered. + * @returns A promise that resolves to a boolean, indicating if the object was rendered. + */ + renderObject = async (objectState: AnyObjectState, force = false): Promise => { let didRender = false; + let renderer = this.renderers.get(objectState.id); + + const isFirstRender = renderer === undefined; + if (objectState.type === 'brush_line') { - let renderer = this.renderers.get(objectState.id); assert(renderer instanceof CanvasBrushLineRenderer || renderer === undefined); if (!renderer) { @@ -83,9 +108,8 @@ export class CanvasObjectRenderer { this.parent.konva.objectGroup.add(renderer.konva.group); } - didRender = renderer.update(objectState, force); + didRender = renderer.update(objectState, force || isFirstRender); } else if (objectState.type === 'eraser_line') { - let renderer = this.renderers.get(objectState.id); assert(renderer instanceof CanvasEraserLineRenderer || renderer === undefined); if (!renderer) { @@ -94,9 +118,8 @@ export class CanvasObjectRenderer { this.parent.konva.objectGroup.add(renderer.konva.group); } - didRender = renderer.update(objectState, force); + didRender = renderer.update(objectState, force || isFirstRender); } else if (objectState.type === 'rect') { - let renderer = this.renderers.get(objectState.id); assert(renderer instanceof CanvasRectRenderer || renderer === undefined); if (!renderer) { @@ -105,9 +128,8 @@ export class CanvasObjectRenderer { this.parent.konva.objectGroup.add(renderer.konva.group); } - didRender = renderer.update(objectState, force); + didRender = renderer.update(objectState, force || isFirstRender); } else if (objectState.type === 'image') { - let renderer = this.renderers.get(objectState.id); assert(renderer instanceof CanvasImageRenderer || renderer === undefined); if (!renderer) { @@ -115,28 +137,43 @@ export class CanvasObjectRenderer { this.renderers.set(renderer.id, renderer); this.parent.konva.objectGroup.add(renderer.konva.group); } - didRender = await renderer.update(objectState, force); + didRender = await renderer.update(objectState, force || isFirstRender); } - this.isFirstRender = false; return didRender; }; + /** + * Determines if the renderer has a buffer object to render. + * @returns Whether the renderer has a buffer object to render. + */ hasBuffer = (): boolean => { return this.buffer !== null; }; + /** + * Sets the buffer object state to render. + * @param objectState The object state to set as the buffer. + * @returns A promise that resolves to a boolean, indicating if the object was rendered. + */ setBuffer = async (objectState: AnyObjectState): Promise => { this.buffer = objectState; return await this.renderObject(this.buffer, true); }; + /** + * Clears the buffer object state. + */ clearBuffer = () => { this.buffer = null; }; + /** + * Commits the current buffer object, pushing the buffer object state back to the application state. + */ commitBuffer = () => { if (!this.buffer) { + this.log.warn('No buffer object to commit'); return; } @@ -181,18 +218,17 @@ export class CanvasObjectRenderer { return needsPixelBbox; }; + /** + * Checks if the renderer has any objects to render, including its buffer. + * @returns Whether the renderer has any objects to render. + */ hasObjects = (): boolean => { return this.renderers.size > 0 || this.buffer !== null; }; - hideAll = (except: string[]) => { - for (const renderer of this.renderers.values()) { - if (!except.includes(renderer.id)) { - renderer.setVisibility(false); - } - } - }; - + /** + * Destroys this renderer and all of its object renderers. + */ destroy = () => { this.log.trace('Destroying object renderer'); for (const renderer of this.renderers.values()) { @@ -201,6 +237,10 @@ export class CanvasObjectRenderer { this.renderers.clear(); }; + /** + * Gets a serializable representation of the renderer. + * @returns A serializable representation of the renderer. + */ repr = () => { return { id: this.id, @@ -208,8 +248,6 @@ export class CanvasObjectRenderer { parent: this.parent.id, renderers: Array.from(this.renderers.values()).map((renderer) => renderer.repr()), buffer: deepClone(this.buffer), - isFirstRender: this.isFirstRender, - isRendering: this.isRendering, }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index 32a91a6a9a..8748379da4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -49,8 +49,8 @@ export class CanvasRectRenderer { this.state = state; } - update(state: CanvasRectState, force = this.isFirstRender): boolean { - if (this.state !== state || force) { + update(state: CanvasRectState, force = false): boolean { + if (force || this.state !== state) { this.isFirstRender = false; this.log.trace({ state }, 'Updating rect');