tidy(ui): clean up control layers renderers, docstrings

This commit is contained in:
psychedelicious 2024-06-04 17:39:47 +10:00
parent 848ca79da8
commit 311e44ad19
3 changed files with 282 additions and 190 deletions

View File

@ -18,6 +18,7 @@ import { debouncedRenderers, renderers as normalRenderers } from 'features/contr
import Konva from 'konva'; import Konva from 'konva';
import type { IRect } from 'konva/lib/types'; import type { IRect } from 'konva/lib/types';
import { memo, useCallback, useLayoutEffect, useMemo, useState } from 'react'; import { memo, useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { getImageDTO } from 'services/api/endpoints/images';
import { useDevicePixelRatio } from 'use-device-pixel-ratio'; import { useDevicePixelRatio } from 'use-device-pixel-ratio';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@ -160,7 +161,7 @@ const useStageRenderer = (
useLayoutEffect(() => { useLayoutEffect(() => {
log.trace('Rendering layers'); log.trace('Rendering layers');
renderers.renderLayers(stage, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged); renderers.renderLayers(stage, state.layers, state.globalMaskLayerOpacity, tool, getImageDTO, onLayerPosChanged);
}, [ }, [
stage, stage,
state.layers, state.layers,

View File

@ -4,6 +4,7 @@ import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
import { isRegionalGuidanceLayer, RG_LAYER_NAME } from 'features/controlLayers/store/controlLayersSlice'; import { isRegionalGuidanceLayer, RG_LAYER_NAME } from 'features/controlLayers/store/controlLayersSlice';
import { renderers } from 'features/controlLayers/util/renderers'; import { renderers } from 'features/controlLayers/util/renderers';
import Konva from 'konva'; import Konva from 'konva';
import { getImageDTO } from 'services/api/endpoints/images';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
/** /**
@ -22,7 +23,7 @@ export const getRegionalPromptLayerBlobs = async (
const reduxLayers = layers.filter(isRegionalGuidanceLayer); const reduxLayers = layers.filter(isRegionalGuidanceLayer);
const container = document.createElement('div'); const container = document.createElement('div');
const stage = new Konva.Stage({ container, width, height }); const stage = new Konva.Stage({ container, width, height });
renderers.renderLayers(stage, reduxLayers, 1, 'brush'); renderers.renderLayers(stage, reduxLayers, 1, 'brush', getImageDTO);
const konvaLayers = stage.find<Konva.Layer>(`.${RG_LAYER_NAME}`); const konvaLayers = stage.find<Konva.Layer>(`.${RG_LAYER_NAME}`);
const blobs: Record<string, Blob> = {}; const blobs: Record<string, Blob> = {};

View File

@ -1,4 +1,3 @@
import { getStore } from 'app/store/nanostores/store';
import { rgbaColorToString, rgbColorToString } from 'features/canvas/util/colorToString'; import { rgbaColorToString, rgbColorToString } from 'features/canvas/util/colorToString';
import { getScaledFlooredCursorPosition, snapPosToStage } from 'features/controlLayers/hooks/mouseEventHooks'; import { getScaledFlooredCursorPosition, snapPosToStage } from 'features/controlLayers/hooks/mouseEventHooks';
import { import {
@ -46,7 +45,7 @@ import Konva from 'konva';
import type { IRect, Vector2d } from 'konva/lib/types'; import type { IRect, Vector2d } from 'konva/lib/types';
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
import type { RgbColor } from 'react-colorful'; import type { RgbColor } from 'react-colorful';
import { imagesApi } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@ -57,26 +56,31 @@ const BRUSH_BORDER_OUTER_COLOR = 'rgba(255,255,255,0.8)';
export const STAGE_BG_DATAURL = export const STAGE_BG_DATAURL =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAEsmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIKICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgZXhpZjpQaXhlbFhEaW1lbnNpb249IjIwIgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iMjAiCiAgIGV4aWY6Q29sb3JTcGFjZT0iMSIKICAgdGlmZjpJbWFnZVdpZHRoPSIyMCIKICAgdGlmZjpJbWFnZUxlbmd0aD0iMjAiCiAgIHRpZmY6UmVzb2x1dGlvblVuaXQ9IjIiCiAgIHRpZmY6WFJlc29sdXRpb249IjMwMC8xIgogICB0aWZmOllSZXNvbHV0aW9uPSIzMDAvMSIKICAgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIKICAgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIgogICB4bXA6TW9kaWZ5RGF0ZT0iMjAyNC0wNC0yM1QwODoyMDo0NysxMDowMCIKICAgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyNC0wNC0yM1QwODoyMDo0NysxMDowMCI+CiAgIDx4bXBNTTpIaXN0b3J5PgogICAgPHJkZjpTZXE+CiAgICAgPHJkZjpsaQogICAgICBzdEV2dDphY3Rpb249InByb2R1Y2VkIgogICAgICBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZmZpbml0eSBQaG90byAxLjEwLjgiCiAgICAgIHN0RXZ0OndoZW49IjIwMjQtMDQtMjNUMDg6MjA6NDcrMTA6MDAiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KPD94cGFja2V0IGVuZD0iciI/Pn9pdVgAAAGBaUNDUHNSR0IgSUVDNjE5NjYtMi4xAAAokXWR3yuDURjHP5uJmKghFy6WxpVpqMWNMgm1tGbKr5vt3S+1d3t73y3JrXKrKHHj1wV/AbfKtVJESq53TdywXs9rakv2nJ7zfM73nOfpnOeAPZJRVMPhAzWb18NTAffC4pK7oYiDTjpw4YgqhjYeCgWpaR8P2Kx457Vq1T73rzXHE4YCtkbhMUXT88LTwsG1vGbxrnC7ko7Ghc+F+3W5oPC9pcfKXLQ4VeYvi/VIeALsbcLuVBXHqlhJ66qwvByPmikov/exXuJMZOfnJPaId2MQZooAbmaYZAI/g4zK7MfLEAOyoka+7yd/lpzkKjJrrKOzSoo0efpFLUj1hMSk6AkZGdat/v/tq5EcHipXdwag/sU033qhYQdK26b5eWyapROoe4arbCU/dwQj76JvVzTPIbRuwsV1RYvtweUWdD1pUT36I9WJ25NJeD2DlkVw3ULTcrlnv/ucPkJkQ77qBvYPoE/Ot658AxagZ8FoS/a7AAAACXBIWXMAAC4jAAAuIwF4pT92AAAAL0lEQVQ4jWM8ffo0A25gYmKCR5YJjxxBMKp5ZGhm/P//Px7pM2fO0MrmUc0jQzMAB2EIhZC3pUYAAAAASUVORK5CYII='; 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAEsmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIKICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgZXhpZjpQaXhlbFhEaW1lbnNpb249IjIwIgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iMjAiCiAgIGV4aWY6Q29sb3JTcGFjZT0iMSIKICAgdGlmZjpJbWFnZVdpZHRoPSIyMCIKICAgdGlmZjpJbWFnZUxlbmd0aD0iMjAiCiAgIHRpZmY6UmVzb2x1dGlvblVuaXQ9IjIiCiAgIHRpZmY6WFJlc29sdXRpb249IjMwMC8xIgogICB0aWZmOllSZXNvbHV0aW9uPSIzMDAvMSIKICAgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIKICAgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIgogICB4bXA6TW9kaWZ5RGF0ZT0iMjAyNC0wNC0yM1QwODoyMDo0NysxMDowMCIKICAgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyNC0wNC0yM1QwODoyMDo0NysxMDowMCI+CiAgIDx4bXBNTTpIaXN0b3J5PgogICAgPHJkZjpTZXE+CiAgICAgPHJkZjpsaQogICAgICBzdEV2dDphY3Rpb249InByb2R1Y2VkIgogICAgICBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZmZpbml0eSBQaG90byAxLjEwLjgiCiAgICAgIHN0RXZ0OndoZW49IjIwMjQtMDQtMjNUMDg6MjA6NDcrMTA6MDAiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KPD94cGFja2V0IGVuZD0iciI/Pn9pdVgAAAGBaUNDUHNSR0IgSUVDNjE5NjYtMi4xAAAokXWR3yuDURjHP5uJmKghFy6WxpVpqMWNMgm1tGbKr5vt3S+1d3t73y3JrXKrKHHj1wV/AbfKtVJESq53TdywXs9rakv2nJ7zfM73nOfpnOeAPZJRVMPhAzWb18NTAffC4pK7oYiDTjpw4YgqhjYeCgWpaR8P2Kx457Vq1T73rzXHE4YCtkbhMUXT88LTwsG1vGbxrnC7ko7Ghc+F+3W5oPC9pcfKXLQ4VeYvi/VIeALsbcLuVBXHqlhJ66qwvByPmikov/exXuJMZOfnJPaId2MQZooAbmaYZAI/g4zK7MfLEAOyoka+7yd/lpzkKjJrrKOzSoo0efpFLUj1hMSk6AkZGdat/v/tq5EcHipXdwag/sU033qhYQdK26b5eWyapROoe4arbCU/dwQj76JvVzTPIbRuwsV1RYvtweUWdD1pUT36I9WJ25NJeD2DlkVw3ULTcrlnv/ucPkJkQ77qBvYPoE/Ot658AxagZ8FoS/a7AAAACXBIWXMAAC4jAAAuIwF4pT92AAAAL0lEQVQ4jWM8ffo0A25gYmKCR5YJjxxBMKp5ZGhm/P//Px7pM2fO0MrmUc0jQzMAB2EIhZC3pUYAAAAASUVORK5CYII=';
const mapId = (object: { id: string }) => object.id; const mapId = (object: { id: string }): string => object.id;
const selectRenderableLayers = (n: Konva.Node) => /**
* Konva selection callback to select all renderable layers. This includes RG, CA and II layers.
*/
const selectRenderableLayers = (n: Konva.Node): boolean =>
n.name() === RG_LAYER_NAME || n.name() === CA_LAYER_NAME || n.name() === INITIAL_IMAGE_LAYER_NAME; n.name() === RG_LAYER_NAME || n.name() === CA_LAYER_NAME || n.name() === INITIAL_IMAGE_LAYER_NAME;
const selectVectorMaskObjects = (node: Konva.Node) => { /**
* Konva selection callback to select RG mask objects. This includes lines and rects.
*/
const selectVectorMaskObjects = (node: Konva.Node): boolean => {
return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME; return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME;
}; };
/** /**
* Creates the brush preview layer. * Creates the singleton tool preview layer and all its objects.
* @param stage The konva stage to render on. * @param stage The konva stage
* @returns The brush preview layer.
*/ */
const createToolPreviewLayer = (stage: Konva.Stage) => { const createToolPreviewLayer = (stage: Konva.Stage): Konva.Layer => {
// Initialize the brush preview layer & add to the stage // Initialize the brush preview layer & add to the stage
const toolPreviewLayer = new Konva.Layer({ id: TOOL_PREVIEW_LAYER_ID, visible: false, listening: false }); const toolPreviewLayer = new Konva.Layer({ id: TOOL_PREVIEW_LAYER_ID, visible: false, listening: false });
stage.add(toolPreviewLayer); stage.add(toolPreviewLayer);
// Add handlers to show/hide the brush preview layer // Add handlers to show/hide the tool preview layer as the mouse enters/leaves the stage
stage.on('mousemove', (e) => { stage.on('mousemove', (e) => {
const tool = $tool.get(); const tool = $tool.get();
e.target e.target
@ -121,7 +125,7 @@ const createToolPreviewLayer = (stage: Konva.Stage) => {
brushPreviewGroup.add(brushPreviewBorderOuter); brushPreviewGroup.add(brushPreviewBorderOuter);
toolPreviewLayer.add(brushPreviewGroup); toolPreviewLayer.add(brushPreviewGroup);
// Create the rect preview // 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({ id: TOOL_PREVIEW_RECT_ID, listening: false, stroke: 'white', strokeWidth: 1 }); const rectPreview = new Konva.Rect({ id: TOOL_PREVIEW_RECT_ID, listening: false, stroke: 'white', strokeWidth: 1 });
toolPreviewLayer.add(rectPreview); toolPreviewLayer.add(rectPreview);
@ -130,12 +134,14 @@ const createToolPreviewLayer = (stage: Konva.Stage) => {
/** /**
* Renders the brush preview for the selected tool. * Renders the brush preview for the selected tool.
* @param stage The konva stage to render on. * @param stage The konva stage
* @param tool The selected tool. * @param tool The selected tool
* @param color The selected layer's color. * @param color The selected layer's color
* @param cursorPos The cursor position. * @param selectedLayerType The selected layer's type
* @param lastMouseDownPos The position of the last mouse down event - used for the rect tool. * @param globalMaskLayerOpacity The global mask layer opacity
* @param brushSize The brush size. * @param cursorPos The cursor position
* @param lastMouseDownPos The position of the last mouse down event - used for the rect tool
* @param brushSize The brush size
*/ */
const renderToolPreview = ( const renderToolPreview = (
stage: Konva.Stage, stage: Konva.Stage,
@ -146,7 +152,7 @@ const renderToolPreview = (
cursorPos: Vector2d | null, cursorPos: Vector2d | null,
lastMouseDownPos: Vector2d | null, lastMouseDownPos: Vector2d | null,
brushSize: number brushSize: number
) => { ): void => {
const layerCount = stage.find(selectRenderableLayers).length; const layerCount = stage.find(selectRenderableLayers).length;
// Update the stage's pointer style // Update the stage's pointer style
if (layerCount === 0) { if (layerCount === 0) {
@ -162,7 +168,7 @@ const renderToolPreview = (
// Move rect gets a crosshair // Move rect gets a crosshair
stage.container().style.cursor = 'crosshair'; stage.container().style.cursor = 'crosshair';
} else { } else {
// Else we use the brush preview // Else we hide the native cursor and use the konva-rendered brush preview
stage.container().style.cursor = 'none'; stage.container().style.cursor = 'none';
} }
@ -227,28 +233,29 @@ const renderToolPreview = (
}; };
/** /**
* Creates a vector mask layer. * Creates a regional guidance layer.
* @param stage The konva stage to attach the layer to. * @param stage The konva stage
* @param reduxLayer The redux layer to create the konva layer from. * @param layerState The regional guidance layer state
* @param onLayerPosChanged Callback for when the layer's position changes. * @param onLayerPosChanged Callback for when the layer's position changes
*/ */
const createRegionalGuidanceLayer = ( const createRGLayer = (
stage: Konva.Stage, stage: Konva.Stage,
reduxLayer: RegionalGuidanceLayer, layerState: RegionalGuidanceLayer,
onLayerPosChanged?: (layerId: string, x: number, y: number) => void onLayerPosChanged?: (layerId: string, x: number, y: number) => void
) => { ): Konva.Layer => {
// This layer hasn't been added to the konva state yet // This layer hasn't been added to the konva state yet
const konvaLayer = new Konva.Layer({ const konvaLayer = new Konva.Layer({
id: reduxLayer.id, id: layerState.id,
name: RG_LAYER_NAME, name: RG_LAYER_NAME,
draggable: true, draggable: true,
dragDistance: 0, dragDistance: 0,
}); });
// Create a `dragmove` listener for this layer // When a drag on the layer finishes, update the layer's position in state. During the drag, konva handles changing
// the position - we do not need to call this on the `dragmove` event.
if (onLayerPosChanged) { if (onLayerPosChanged) {
konvaLayer.on('dragend', function (e) { konvaLayer.on('dragend', function (e) {
onLayerPosChanged(reduxLayer.id, Math.floor(e.target.x()), Math.floor(e.target.y())); onLayerPosChanged(layerState.id, Math.floor(e.target.x()), Math.floor(e.target.y()));
}); });
} }
@ -258,7 +265,7 @@ const createRegionalGuidanceLayer = (
if (!cursorPos) { if (!cursorPos) {
return this.getAbsolutePosition(); return this.getAbsolutePosition();
} }
// Prevent the user from dragging the layer out of the stage bounds. // Prevent the user from dragging the layer out of the stage bounds by constaining the cursor position to the stage bounds
if ( if (
cursorPos.x < 0 || cursorPos.x < 0 ||
cursorPos.x > stage.width() / stage.scaleX() || cursorPos.x > stage.width() / stage.scaleX() ||
@ -272,7 +279,7 @@ const createRegionalGuidanceLayer = (
// The object group holds all of the layer's objects (e.g. lines and rects) // The object group holds all of the layer's objects (e.g. lines and rects)
const konvaObjectGroup = new Konva.Group({ const konvaObjectGroup = new Konva.Group({
id: getRGLayerObjectGroupId(reduxLayer.id, uuidv4()), id: getRGLayerObjectGroupId(layerState.id, uuidv4()),
name: RG_LAYER_OBJECT_GROUP_NAME, name: RG_LAYER_OBJECT_GROUP_NAME,
listening: false, listening: false,
}); });
@ -284,47 +291,51 @@ const createRegionalGuidanceLayer = (
}; };
/** /**
* Creates a konva line from a redux vector mask line. * Creates a konva line from a vector mask line.
* @param reduxObject The redux object to create the konva line from. * @param vectorMaskLine The vector mask line state
* @param konvaGroup The konva group to add the line to. * @param layerObjectGroup The konva layer's object group to add the line to
*/ */
const createVectorMaskLine = (reduxObject: VectorMaskLine, konvaGroup: Konva.Group): Konva.Line => { const createVectorMaskLine = (vectorMaskLine: VectorMaskLine, layerObjectGroup: Konva.Group): Konva.Line => {
const vectorMaskLine = new Konva.Line({ const konvaLine = new Konva.Line({
id: reduxObject.id, id: vectorMaskLine.id,
key: reduxObject.id, key: vectorMaskLine.id,
name: RG_LAYER_LINE_NAME, name: RG_LAYER_LINE_NAME,
strokeWidth: reduxObject.strokeWidth, strokeWidth: vectorMaskLine.strokeWidth,
tension: 0, tension: 0,
lineCap: 'round', lineCap: 'round',
lineJoin: 'round', lineJoin: 'round',
shadowForStrokeEnabled: false, shadowForStrokeEnabled: false,
globalCompositeOperation: reduxObject.tool === 'brush' ? 'source-over' : 'destination-out', globalCompositeOperation: vectorMaskLine.tool === 'brush' ? 'source-over' : 'destination-out',
listening: false, listening: false,
}); });
konvaGroup.add(vectorMaskLine); layerObjectGroup.add(konvaLine);
return vectorMaskLine; return konvaLine;
}; };
/** /**
* Creates a konva rect from a redux vector mask rect. * Creates a konva rect from a vector mask rect.
* @param reduxObject The redux object to create the konva rect from. * @param vectorMaskRect The vector mask rect state
* @param konvaGroup The konva group to add the rect to. * @param layerObjectGroup The konva layer's object group to add the line to
*/ */
const createVectorMaskRect = (reduxObject: VectorMaskRect, konvaGroup: Konva.Group): Konva.Rect => { const createVectorMaskRect = (vectorMaskRect: VectorMaskRect, layerObjectGroup: Konva.Group): Konva.Rect => {
const vectorMaskRect = new Konva.Rect({ const konvaRect = new Konva.Rect({
id: reduxObject.id, id: vectorMaskRect.id,
key: reduxObject.id, key: vectorMaskRect.id,
name: RG_LAYER_RECT_NAME, name: RG_LAYER_RECT_NAME,
x: reduxObject.x, x: vectorMaskRect.x,
y: reduxObject.y, y: vectorMaskRect.y,
width: reduxObject.width, width: vectorMaskRect.width,
height: reduxObject.height, height: vectorMaskRect.height,
listening: false, listening: false,
}); });
konvaGroup.add(vectorMaskRect); layerObjectGroup.add(konvaRect);
return vectorMaskRect; return konvaRect;
}; };
/**
* Creates the "compositing rect" for a layer.
* @param konvaLayer The konva layer
*/
const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => {
const compositingRect = new Konva.Rect({ name: COMPOSITING_RECT_NAME, listening: false }); const compositingRect = new Konva.Rect({ name: COMPOSITING_RECT_NAME, listening: false });
konvaLayer.add(compositingRect); konvaLayer.add(compositingRect);
@ -332,41 +343,41 @@ const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => {
}; };
/** /**
* Renders a vector mask layer. * Renders a regional guidance layer.
* @param stage The konva stage to render on. * @param stage The konva stage
* @param reduxLayer The redux vector mask layer to render. * @param layerState The regional guidance layer state
* @param reduxLayerIndex The index of the layer in the redux store. * @param globalMaskLayerOpacity The global mask layer opacity
* @param globalMaskLayerOpacity The opacity of the global mask layer. * @param tool The current tool
* @param tool The current tool. * @param onLayerPosChanged Callback for when the layer's position changes
*/ */
const renderRegionalGuidanceLayer = ( const renderRGLayer = (
stage: Konva.Stage, stage: Konva.Stage,
reduxLayer: RegionalGuidanceLayer, layerState: RegionalGuidanceLayer,
globalMaskLayerOpacity: number, globalMaskLayerOpacity: number,
tool: Tool, tool: Tool,
onLayerPosChanged?: (layerId: string, x: number, y: number) => void onLayerPosChanged?: (layerId: string, x: number, y: number) => void
): void => { ): void => {
const konvaLayer = const konvaLayer =
stage.findOne<Konva.Layer>(`#${reduxLayer.id}`) ?? stage.findOne<Konva.Layer>(`#${layerState.id}`) ?? createRGLayer(stage, layerState, onLayerPosChanged);
createRegionalGuidanceLayer(stage, reduxLayer, onLayerPosChanged);
// Update the layer's position and listening state // Update the layer's position and listening state
konvaLayer.setAttrs({ konvaLayer.setAttrs({
listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events
x: Math.floor(reduxLayer.x), x: Math.floor(layerState.x),
y: Math.floor(reduxLayer.y), y: Math.floor(layerState.y),
}); });
// Convert the color to a string, stripping the alpha - the object group will handle opacity. // Convert the color to a string, stripping the alpha - the object group will handle opacity.
const rgbColor = rgbColorToString(reduxLayer.previewColor); const rgbColor = rgbColorToString(layerState.previewColor);
const konvaObjectGroup = konvaLayer.findOne<Konva.Group>(`.${RG_LAYER_OBJECT_GROUP_NAME}`); const konvaObjectGroup = konvaLayer.findOne<Konva.Group>(`.${RG_LAYER_OBJECT_GROUP_NAME}`);
assert(konvaObjectGroup, `Object group not found for layer ${reduxLayer.id}`); assert(konvaObjectGroup, `Object group not found for layer ${layerState.id}`);
// We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required.
let groupNeedsCache = false; let groupNeedsCache = false;
const objectIds = reduxLayer.maskObjects.map(mapId); const objectIds = layerState.maskObjects.map(mapId);
// Destroy any objects that are no longer in the redux state
for (const objectNode of konvaObjectGroup.find(selectVectorMaskObjects)) { for (const objectNode of konvaObjectGroup.find(selectVectorMaskObjects)) {
if (!objectIds.includes(objectNode.id())) { if (!objectIds.includes(objectNode.id())) {
objectNode.destroy(); objectNode.destroy();
@ -374,15 +385,15 @@ const renderRegionalGuidanceLayer = (
} }
} }
for (const reduxObject of reduxLayer.maskObjects) { for (const maskObject of layerState.maskObjects) {
if (reduxObject.type === 'vector_mask_line') { if (maskObject.type === 'vector_mask_line') {
const vectorMaskLine = const vectorMaskLine =
stage.findOne<Konva.Line>(`#${reduxObject.id}`) ?? createVectorMaskLine(reduxObject, konvaObjectGroup); stage.findOne<Konva.Line>(`#${maskObject.id}`) ?? createVectorMaskLine(maskObject, konvaObjectGroup);
// Only update the points if they have changed. The point values are never mutated, they are only added to the // 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. // array, so checking the length is sufficient to determine if we need to re-cache.
if (vectorMaskLine.points().length !== reduxObject.points.length) { if (vectorMaskLine.points().length !== maskObject.points.length) {
vectorMaskLine.points(reduxObject.points); vectorMaskLine.points(maskObject.points);
groupNeedsCache = true; groupNeedsCache = true;
} }
// Only update the color if it has changed. // Only update the color if it has changed.
@ -390,9 +401,9 @@ const renderRegionalGuidanceLayer = (
vectorMaskLine.stroke(rgbColor); vectorMaskLine.stroke(rgbColor);
groupNeedsCache = true; groupNeedsCache = true;
} }
} else if (reduxObject.type === 'vector_mask_rect') { } else if (maskObject.type === 'vector_mask_rect') {
const konvaObject = const konvaObject =
stage.findOne<Konva.Rect>(`#${reduxObject.id}`) ?? createVectorMaskRect(reduxObject, konvaObjectGroup); stage.findOne<Konva.Rect>(`#${maskObject.id}`) ?? createVectorMaskRect(maskObject, konvaObjectGroup);
// Only update the color if it has changed. // Only update the color if it has changed.
if (konvaObject.fill() !== rgbColor) { if (konvaObject.fill() !== rgbColor) {
@ -403,8 +414,8 @@ const renderRegionalGuidanceLayer = (
} }
// Only update layer visibility if it has changed. // Only update layer visibility if it has changed.
if (konvaLayer.visible() !== reduxLayer.isEnabled) { if (konvaLayer.visible() !== layerState.isEnabled) {
konvaLayer.visible(reduxLayer.isEnabled); konvaLayer.visible(layerState.isEnabled);
groupNeedsCache = true; groupNeedsCache = true;
} }
@ -428,7 +439,7 @@ const renderRegionalGuidanceLayer = (
* Instead, with the special handling, the effect is as if you drew all the shapes at 100% opacity, flattened them to * Instead, with the special handling, the effect is as if you drew all the shapes at 100% opacity, flattened them to
* a single raster image, and _then_ applied the 50% opacity. * a single raster image, and _then_ applied the 50% opacity.
*/ */
if (reduxLayer.isSelected && tool !== 'move') { if (layerState.isSelected && tool !== 'move') {
// We must clear the cache first so Konva will re-draw the group with the new compositing rect // We must clear the cache first so Konva will re-draw the group with the new compositing rect
if (konvaObjectGroup.isCached()) { if (konvaObjectGroup.isCached()) {
konvaObjectGroup.clearCache(); konvaObjectGroup.clearCache();
@ -438,7 +449,7 @@ const renderRegionalGuidanceLayer = (
compositingRect.setAttrs({ 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 // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already
...(!reduxLayer.bboxNeedsUpdate && reduxLayer.bbox ? reduxLayer.bbox : getLayerBboxFast(konvaLayer)), ...(!layerState.bboxNeedsUpdate && layerState.bbox ? layerState.bbox : getLayerBboxFast(konvaLayer)),
fill: rgbColor, fill: rgbColor,
opacity: globalMaskLayerOpacity, opacity: globalMaskLayerOpacity,
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
@ -459,9 +470,14 @@ const renderRegionalGuidanceLayer = (
} }
}; };
const createInitialImageLayer = (stage: Konva.Stage, reduxLayer: InitialImageLayer): Konva.Layer => { /**
* Creates an initial image konva layer.
* @param stage The konva stage
* @param layerState The initial image layer state
*/
const createIILayer = (stage: Konva.Stage, layerState: InitialImageLayer): Konva.Layer => {
const konvaLayer = new Konva.Layer({ const konvaLayer = new Konva.Layer({
id: reduxLayer.id, id: layerState.id,
name: INITIAL_IMAGE_LAYER_NAME, name: INITIAL_IMAGE_LAYER_NAME,
imageSmoothingEnabled: true, imageSmoothingEnabled: true,
listening: false, listening: false,
@ -470,20 +486,27 @@ const createInitialImageLayer = (stage: Konva.Stage, reduxLayer: InitialImageLay
return konvaLayer; return konvaLayer;
}; };
const createInitialImageLayerImage = (konvaLayer: Konva.Layer, image: HTMLImageElement): Konva.Image => { /**
* Creates the konva image for an initial image layer.
* @param konvaLayer The konva layer
* @param imageEl The image element
*/
const createIILayerImage = (konvaLayer: Konva.Layer, imageEl: HTMLImageElement): Konva.Image => {
const konvaImage = new Konva.Image({ const konvaImage = new Konva.Image({
name: INITIAL_IMAGE_LAYER_IMAGE_NAME, name: INITIAL_IMAGE_LAYER_IMAGE_NAME,
image, image: imageEl,
}); });
konvaLayer.add(konvaImage); konvaLayer.add(konvaImage);
return konvaImage; return konvaImage;
}; };
const updateInitialImageLayerImageAttrs = ( /**
stage: Konva.Stage, * Updates an initial image layer's attributes (width, height, opacity, visibility).
konvaImage: Konva.Image, * @param stage The konva stage
reduxLayer: InitialImageLayer * @param konvaImage The konva image
) => { * @param layerState The initial image layer state
*/
const updateIILayerImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, layerState: InitialImageLayer): void => {
// Konva erroneously reports NaN for width and height when the stage is hidden. This causes errors when caching, // Konva erroneously reports NaN for width and height when the stage is hidden. This causes errors when caching,
// but it doesn't seem to break anything. // but it doesn't seem to break anything.
// TODO(psyche): Investigate and report upstream. // TODO(psyche): Investigate and report upstream.
@ -492,46 +515,55 @@ const updateInitialImageLayerImageAttrs = (
if ( if (
konvaImage.width() !== newWidth || konvaImage.width() !== newWidth ||
konvaImage.height() !== newHeight || konvaImage.height() !== newHeight ||
konvaImage.visible() !== reduxLayer.isEnabled konvaImage.visible() !== layerState.isEnabled
) { ) {
konvaImage.setAttrs({ konvaImage.setAttrs({
opacity: reduxLayer.opacity, opacity: layerState.opacity,
scaleX: 1, scaleX: 1,
scaleY: 1, scaleY: 1,
width: stage.width() / stage.scaleX(), width: stage.width() / stage.scaleX(),
height: stage.height() / stage.scaleY(), height: stage.height() / stage.scaleY(),
visible: reduxLayer.isEnabled, visible: layerState.isEnabled,
}); });
} }
if (konvaImage.opacity() !== reduxLayer.opacity) { if (konvaImage.opacity() !== layerState.opacity) {
konvaImage.opacity(reduxLayer.opacity); konvaImage.opacity(layerState.opacity);
} }
}; };
const updateInitialImageLayerImageSource = async ( /**
* Update an initial image layer's image source when the image changes.
* @param stage The konva stage
* @param konvaLayer The konva layer
* @param layerState The initial image layer state
* @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source
*/
const updateIILayerImageSource = async (
stage: Konva.Stage, stage: Konva.Stage,
konvaLayer: Konva.Layer, konvaLayer: Konva.Layer,
reduxLayer: InitialImageLayer layerState: InitialImageLayer,
) => { getImageDTO: (imageName: string) => Promise<ImageDTO | null>
if (reduxLayer.image) { ): Promise<void> => {
const imageName = reduxLayer.image.name; if (layerState.image) {
const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName)); const imageName = layerState.image.name;
const imageDTO = await req.unwrap(); const imageDTO = await getImageDTO(imageName);
req.unsubscribe(); if (!imageDTO) {
return;
}
const imageEl = new Image(); const imageEl = new Image();
const imageId = getIILayerImageId(reduxLayer.id, imageName); const imageId = getIILayerImageId(layerState.id, imageName);
imageEl.onload = () => { imageEl.onload = () => {
// Find the existing image or create a new one - must find using the name, bc the id may have just changed // Find the existing image or create a new one - must find using the name, bc the id may have just changed
const konvaImage = const konvaImage =
konvaLayer.findOne<Konva.Image>(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`) ?? konvaLayer.findOne<Konva.Image>(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`) ??
createInitialImageLayerImage(konvaLayer, imageEl); createIILayerImage(konvaLayer, imageEl);
// Update the image's attributes // Update the image's attributes
konvaImage.setAttrs({ konvaImage.setAttrs({
id: imageId, id: imageId,
image: imageEl, image: imageEl,
}); });
updateInitialImageLayerImageAttrs(stage, konvaImage, reduxLayer); updateIILayerImageAttrs(stage, konvaImage, layerState);
imageEl.id = imageId; imageEl.id = imageId;
}; };
imageEl.src = imageDTO.image_url; imageEl.src = imageDTO.image_url;
@ -540,14 +572,24 @@ const updateInitialImageLayerImageSource = async (
} }
}; };
const renderInitialImageLayer = (stage: Konva.Stage, reduxLayer: InitialImageLayer) => { /**
const konvaLayer = stage.findOne<Konva.Layer>(`#${reduxLayer.id}`) ?? createInitialImageLayer(stage, reduxLayer); * Renders an initial image layer.
* @param stage The konva stage
* @param layerState The initial image layer state
* @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source
*/
const renderIILayer = (
stage: Konva.Stage,
layerState: InitialImageLayer,
getImageDTO: (imageName: string) => Promise<ImageDTO | null>
): void => {
const konvaLayer = stage.findOne<Konva.Layer>(`#${layerState.id}`) ?? createIILayer(stage, layerState);
const konvaImage = konvaLayer.findOne<Konva.Image>(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`); const konvaImage = konvaLayer.findOne<Konva.Image>(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`);
const canvasImageSource = konvaImage?.image(); const canvasImageSource = konvaImage?.image();
let imageSourceNeedsUpdate = false; let imageSourceNeedsUpdate = false;
if (canvasImageSource instanceof HTMLImageElement) { if (canvasImageSource instanceof HTMLImageElement) {
const image = reduxLayer.image; const image = layerState.image;
if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.name)) { if (image && canvasImageSource.id !== getCALayerImageId(layerState.id, image.name)) {
imageSourceNeedsUpdate = true; imageSourceNeedsUpdate = true;
} else if (!image) { } else if (!image) {
imageSourceNeedsUpdate = true; imageSourceNeedsUpdate = true;
@ -557,15 +599,20 @@ const renderInitialImageLayer = (stage: Konva.Stage, reduxLayer: InitialImageLay
} }
if (imageSourceNeedsUpdate) { if (imageSourceNeedsUpdate) {
updateInitialImageLayerImageSource(stage, konvaLayer, reduxLayer); updateIILayerImageSource(stage, konvaLayer, layerState, getImageDTO);
} else if (konvaImage) { } else if (konvaImage) {
updateInitialImageLayerImageAttrs(stage, konvaImage, reduxLayer); updateIILayerImageAttrs(stage, konvaImage, layerState);
} }
}; };
const createControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLayer): Konva.Layer => { /**
* Creates a control adapter layer.
* @param stage The konva stage
* @param layerState The control adapter layer state
*/
const createCALayer = (stage: Konva.Stage, layerState: ControlAdapterLayer): Konva.Layer => {
const konvaLayer = new Konva.Layer({ const konvaLayer = new Konva.Layer({
id: reduxLayer.id, id: layerState.id,
name: CA_LAYER_NAME, name: CA_LAYER_NAME,
imageSmoothingEnabled: true, imageSmoothingEnabled: true,
listening: false, listening: false,
@ -574,39 +621,53 @@ const createControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLay
return konvaLayer; return konvaLayer;
}; };
const createControlNetLayerImage = (konvaLayer: Konva.Layer, image: HTMLImageElement): Konva.Image => { /**
* Creates a control adapter layer image.
* @param konvaLayer The konva layer
* @param imageEl The image element
*/
const createCALayerImage = (konvaLayer: Konva.Layer, imageEl: HTMLImageElement): Konva.Image => {
const konvaImage = new Konva.Image({ const konvaImage = new Konva.Image({
name: CA_LAYER_IMAGE_NAME, name: CA_LAYER_IMAGE_NAME,
image, image: imageEl,
}); });
konvaLayer.add(konvaImage); konvaLayer.add(konvaImage);
return konvaImage; return konvaImage;
}; };
const updateControlNetLayerImageSource = async ( /**
* Updates the image source for a control adapter layer. This includes loading the image from the server and updating the konva image.
* @param stage The konva stage
* @param konvaLayer The konva layer
* @param layerState The control adapter layer state
* @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source
*/
const updateCALayerImageSource = async (
stage: Konva.Stage, stage: Konva.Stage,
konvaLayer: Konva.Layer, konvaLayer: Konva.Layer,
reduxLayer: ControlAdapterLayer layerState: ControlAdapterLayer,
) => { getImageDTO: (imageName: string) => Promise<ImageDTO | null>
const image = reduxLayer.controlAdapter.processedImage ?? reduxLayer.controlAdapter.image; ): Promise<void> => {
const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image;
if (image) { if (image) {
const imageName = image.name; const imageName = image.name;
const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName)); const imageDTO = await getImageDTO(imageName);
const imageDTO = await req.unwrap(); if (!imageDTO) {
req.unsubscribe(); return;
}
const imageEl = new Image(); const imageEl = new Image();
const imageId = getCALayerImageId(reduxLayer.id, imageName); const imageId = getCALayerImageId(layerState.id, imageName);
imageEl.onload = () => { imageEl.onload = () => {
// Find the existing image or create a new one - must find using the name, bc the id may have just changed // Find the existing image or create a new one - must find using the name, bc the id may have just changed
const konvaImage = const konvaImage =
konvaLayer.findOne<Konva.Image>(`.${CA_LAYER_IMAGE_NAME}`) ?? createControlNetLayerImage(konvaLayer, imageEl); konvaLayer.findOne<Konva.Image>(`.${CA_LAYER_IMAGE_NAME}`) ?? createCALayerImage(konvaLayer, imageEl);
// Update the image's attributes // Update the image's attributes
konvaImage.setAttrs({ konvaImage.setAttrs({
id: imageId, id: imageId,
image: imageEl, image: imageEl,
}); });
updateControlNetLayerImageAttrs(stage, konvaImage, reduxLayer); updateCALayerImageAttrs(stage, konvaImage, layerState);
// Must cache after this to apply the filters // Must cache after this to apply the filters
konvaImage.cache(); konvaImage.cache();
imageEl.id = imageId; imageEl.id = imageId;
@ -617,11 +678,17 @@ const updateControlNetLayerImageSource = async (
} }
}; };
const updateControlNetLayerImageAttrs = ( /**
* Updates the image attributes for a control adapter layer's image (width, height, visibility, opacity, filters).
* @param stage The konva stage
* @param konvaImage The konva image
* @param layerState The control adapter layer state
*/
const updateCALayerImageAttrs = (
stage: Konva.Stage, stage: Konva.Stage,
konvaImage: Konva.Image, konvaImage: Konva.Image,
reduxLayer: ControlAdapterLayer layerState: ControlAdapterLayer
) => { ): void => {
let needsCache = false; let needsCache = false;
// Konva erroneously reports NaN for width and height when the stage is hidden. This causes errors when caching, // Konva erroneously reports NaN for width and height when the stage is hidden. This causes errors when caching,
// but it doesn't seem to break anything. // but it doesn't seem to break anything.
@ -632,36 +699,47 @@ const updateControlNetLayerImageAttrs = (
if ( if (
konvaImage.width() !== newWidth || konvaImage.width() !== newWidth ||
konvaImage.height() !== newHeight || konvaImage.height() !== newHeight ||
konvaImage.visible() !== reduxLayer.isEnabled || konvaImage.visible() !== layerState.isEnabled ||
hasFilter !== reduxLayer.isFilterEnabled hasFilter !== layerState.isFilterEnabled
) { ) {
konvaImage.setAttrs({ konvaImage.setAttrs({
opacity: reduxLayer.opacity, opacity: layerState.opacity,
scaleX: 1, scaleX: 1,
scaleY: 1, scaleY: 1,
width: stage.width() / stage.scaleX(), width: stage.width() / stage.scaleX(),
height: stage.height() / stage.scaleY(), height: stage.height() / stage.scaleY(),
visible: reduxLayer.isEnabled, visible: layerState.isEnabled,
filters: reduxLayer.isFilterEnabled ? [LightnessToAlphaFilter] : [], filters: layerState.isFilterEnabled ? [LightnessToAlphaFilter] : [],
}); });
needsCache = true; needsCache = true;
} }
if (konvaImage.opacity() !== reduxLayer.opacity) { if (konvaImage.opacity() !== layerState.opacity) {
konvaImage.opacity(reduxLayer.opacity); konvaImage.opacity(layerState.opacity);
} }
if (needsCache) { if (needsCache) {
konvaImage.cache(); konvaImage.cache();
} }
}; };
const renderControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLayer) => { /**
const konvaLayer = stage.findOne<Konva.Layer>(`#${reduxLayer.id}`) ?? createControlNetLayer(stage, reduxLayer); * Renders a control adapter layer. If the layer doesn't already exist, it is created. Otherwise, the layer is updated
* with the current image source and attributes.
* @param stage The konva stage
* @param layerState The control adapter layer state
* @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source
*/
const renderCALayer = (
stage: Konva.Stage,
layerState: ControlAdapterLayer,
getImageDTO: (imageName: string) => Promise<ImageDTO | null>
): void => {
const konvaLayer = stage.findOne<Konva.Layer>(`#${layerState.id}`) ?? createCALayer(stage, layerState);
const konvaImage = konvaLayer.findOne<Konva.Image>(`.${CA_LAYER_IMAGE_NAME}`); const konvaImage = konvaLayer.findOne<Konva.Image>(`.${CA_LAYER_IMAGE_NAME}`);
const canvasImageSource = konvaImage?.image(); const canvasImageSource = konvaImage?.image();
let imageSourceNeedsUpdate = false; let imageSourceNeedsUpdate = false;
if (canvasImageSource instanceof HTMLImageElement) { if (canvasImageSource instanceof HTMLImageElement) {
const image = reduxLayer.controlAdapter.processedImage ?? reduxLayer.controlAdapter.image; const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image;
if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.name)) { if (image && canvasImageSource.id !== getCALayerImageId(layerState.id, image.name)) {
imageSourceNeedsUpdate = true; imageSourceNeedsUpdate = true;
} else if (!image) { } else if (!image) {
imageSourceNeedsUpdate = true; imageSourceNeedsUpdate = true;
@ -671,44 +749,46 @@ const renderControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLay
} }
if (imageSourceNeedsUpdate) { if (imageSourceNeedsUpdate) {
updateControlNetLayerImageSource(stage, konvaLayer, reduxLayer); updateCALayerImageSource(stage, konvaLayer, layerState, getImageDTO);
} else if (konvaImage) { } else if (konvaImage) {
updateControlNetLayerImageAttrs(stage, konvaImage, reduxLayer); updateCALayerImageAttrs(stage, konvaImage, layerState);
} }
}; };
/** /**
* Renders the layers on the stage. * Renders the layers on the stage.
* @param stage The konva stage to render on. * @param stage The konva stage
* @param reduxLayers Array of the layers from the redux store. * @param layerStates Array of all layer states
* @param layerOpacity The opacity of the layer. * @param globalMaskLayerOpacity The global mask layer opacity
* @param onLayerPosChanged Callback for when the layer's position changes. This is optional to allow for offscreen rendering. * @param tool The current tool
* @returns * @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source
* @param onLayerPosChanged Callback for when the layer's position changes
*/ */
const renderLayers = ( const renderLayers = (
stage: Konva.Stage, stage: Konva.Stage,
reduxLayers: Layer[], layerStates: Layer[],
globalMaskLayerOpacity: number, globalMaskLayerOpacity: number,
tool: Tool, tool: Tool,
getImageDTO: (imageName: string) => Promise<ImageDTO | null>,
onLayerPosChanged?: (layerId: string, x: number, y: number) => void onLayerPosChanged?: (layerId: string, x: number, y: number) => void
) => { ): void => {
const reduxLayerIds = reduxLayers.filter(isRenderableLayer).map(mapId); const layerIds = layerStates.filter(isRenderableLayer).map(mapId);
// Remove un-rendered layers // Remove un-rendered layers
for (const konvaLayer of stage.find<Konva.Layer>(selectRenderableLayers)) { for (const konvaLayer of stage.find<Konva.Layer>(selectRenderableLayers)) {
if (!reduxLayerIds.includes(konvaLayer.id())) { if (!layerIds.includes(konvaLayer.id())) {
konvaLayer.destroy(); konvaLayer.destroy();
} }
} }
for (const reduxLayer of reduxLayers) { for (const layer of layerStates) {
if (isRegionalGuidanceLayer(reduxLayer)) { if (isRegionalGuidanceLayer(layer)) {
renderRegionalGuidanceLayer(stage, reduxLayer, globalMaskLayerOpacity, tool, onLayerPosChanged); renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, onLayerPosChanged);
} }
if (isControlAdapterLayer(reduxLayer)) { if (isControlAdapterLayer(layer)) {
renderControlNetLayer(stage, reduxLayer); renderCALayer(stage, layer, getImageDTO);
} }
if (isInitialImageLayer(reduxLayer)) { if (isInitialImageLayer(layer)) {
renderInitialImageLayer(stage, reduxLayer); renderIILayer(stage, layer, getImageDTO);
} }
// IP Adapter layers are not rendered // IP Adapter layers are not rendered
} }
@ -716,13 +796,12 @@ const renderLayers = (
/** /**
* Creates a bounding box rect for a layer. * Creates a bounding box rect for a layer.
* @param reduxLayer The redux layer to create the bounding box for. * @param layerState The layer state for the layer to create the bounding box for
* @param konvaLayer The konva layer to attach the bounding box to. * @param konvaLayer The konva layer to attach the bounding box to
* @param onBboxMouseDown Callback for when the bounding box is clicked.
*/ */
const createBboxRect = (reduxLayer: Layer, konvaLayer: Konva.Layer) => { const createBboxRect = (layerState: Layer, konvaLayer: Konva.Layer): Konva.Rect => {
const rect = new Konva.Rect({ const rect = new Konva.Rect({
id: getLayerBboxId(reduxLayer.id), id: getLayerBboxId(layerState.id),
name: LAYER_BBOX_NAME, name: LAYER_BBOX_NAME,
strokeWidth: 1, strokeWidth: 1,
visible: false, visible: false,
@ -733,12 +812,12 @@ const createBboxRect = (reduxLayer: Layer, konvaLayer: Konva.Layer) => {
/** /**
* Renders the bounding boxes for the layers. * Renders the bounding boxes for the layers.
* @param stage The konva stage to render on * @param stage The konva stage
* @param reduxLayers An array of all redux layers to draw bboxes for * @param layerStates An array of layers to draw bboxes for
* @param tool The current tool * @param tool The current tool
* @returns * @returns
*/ */
const renderBboxes = (stage: Konva.Stage, reduxLayers: Layer[], tool: Tool) => { const renderBboxes = (stage: Konva.Stage, layerStates: Layer[], tool: Tool): void => {
// Hide all bboxes so they don't interfere with getClientRect // Hide all bboxes so they don't interfere with getClientRect
for (const bboxRect of stage.find<Konva.Rect>(`.${LAYER_BBOX_NAME}`)) { for (const bboxRect of stage.find<Konva.Rect>(`.${LAYER_BBOX_NAME}`)) {
bboxRect.visible(false); bboxRect.visible(false);
@ -749,39 +828,39 @@ const renderBboxes = (stage: Konva.Stage, reduxLayers: Layer[], tool: Tool) => {
return; return;
} }
for (const reduxLayer of reduxLayers.filter(isRegionalGuidanceLayer)) { for (const layer of layerStates.filter(isRegionalGuidanceLayer)) {
if (!reduxLayer.bbox) { if (!layer.bbox) {
continue; continue;
} }
const konvaLayer = stage.findOne<Konva.Layer>(`#${reduxLayer.id}`); const konvaLayer = stage.findOne<Konva.Layer>(`#${layer.id}`);
assert(konvaLayer, `Layer ${reduxLayer.id} not found in stage`); assert(konvaLayer, `Layer ${layer.id} not found in stage`);
const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(reduxLayer, konvaLayer); const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layer, konvaLayer);
bboxRect.setAttrs({ bboxRect.setAttrs({
visible: !reduxLayer.bboxNeedsUpdate, visible: !layer.bboxNeedsUpdate,
listening: reduxLayer.isSelected, listening: layer.isSelected,
x: reduxLayer.bbox.x, x: layer.bbox.x,
y: reduxLayer.bbox.y, y: layer.bbox.y,
width: reduxLayer.bbox.width, width: layer.bbox.width,
height: reduxLayer.bbox.height, height: layer.bbox.height,
stroke: reduxLayer.isSelected ? BBOX_SELECTED_STROKE : '', stroke: layer.isSelected ? BBOX_SELECTED_STROKE : '',
}); });
} }
}; };
/** /**
* Calculates the bbox of each regional guidance layer. Only calculates if the mask has changed. * Calculates the bbox of each regional guidance layer. Only calculates if the mask has changed.
* @param stage The konva stage to render on. * @param stage The konva stage
* @param reduxLayers An array of redux layers to calculate bboxes for * @param layerStates An array of layers to calculate bboxes for
* @param onBboxChanged Callback for when the bounding box changes * @param onBboxChanged Callback for when the bounding box changes
*/ */
const updateBboxes = ( const updateBboxes = (
stage: Konva.Stage, stage: Konva.Stage,
reduxLayers: Layer[], layerStates: Layer[],
onBboxChanged: (layerId: string, bbox: IRect | null) => void onBboxChanged: (layerId: string, bbox: IRect | null) => void
) => { ): void => {
for (const rgLayer of reduxLayers.filter(isRegionalGuidanceLayer)) { for (const rgLayer of layerStates.filter(isRegionalGuidanceLayer)) {
const konvaLayer = stage.findOne<Konva.Layer>(`#${rgLayer.id}`); const konvaLayer = stage.findOne<Konva.Layer>(`#${rgLayer.id}`);
assert(konvaLayer, `Layer ${rgLayer.id} not found in stage`); assert(konvaLayer, `Layer ${rgLayer.id} not found in stage`);
// We only need to recalculate the bbox if the layer has changed // We only need to recalculate the bbox if the layer has changed
@ -808,7 +887,7 @@ const updateBboxes = (
/** /**
* Creates the background layer for the stage. * Creates the background layer for the stage.
* @param stage The konva stage to render on * @param stage The konva stage
*/ */
const createBackgroundLayer = (stage: Konva.Stage): Konva.Layer => { const createBackgroundLayer = (stage: Konva.Stage): Konva.Layer => {
const layer = new Konva.Layer({ const layer = new Konva.Layer({
@ -835,11 +914,11 @@ const createBackgroundLayer = (stage: Konva.Stage): Konva.Layer => {
/** /**
* Renders the background layer for the stage. * Renders the background layer for the stage.
* @param stage The konva stage to render on * @param stage The konva stage
* @param width The unscaled width of the canvas * @param width The unscaled width of the canvas
* @param height The unscaled height of the canvas * @param height The unscaled height of the canvas
*/ */
const renderBackground = (stage: Konva.Stage, width: number, height: number) => { const renderBackground = (stage: Konva.Stage, width: number, height: number): void => {
const layer = stage.findOne<Konva.Layer>(`#${BACKGROUND_LAYER_ID}`) ?? createBackgroundLayer(stage); const layer = stage.findOne<Konva.Layer>(`#${BACKGROUND_LAYER_ID}`) ?? createBackgroundLayer(stage);
const background = layer.findOne<Konva.Rect>(`#${BACKGROUND_RECT_ID}`); const background = layer.findOne<Konva.Rect>(`#${BACKGROUND_RECT_ID}`);
@ -880,6 +959,10 @@ const arrangeLayers = (stage: Konva.Stage, layerIds: string[]): void => {
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.zIndex(nextZIndex++); stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.zIndex(nextZIndex++);
}; };
/**
* Creates the "no layers" fallback layer
* @param stage The konva stage
*/
const createNoLayersMessageLayer = (stage: Konva.Stage): Konva.Layer => { const createNoLayersMessageLayer = (stage: Konva.Stage): Konva.Layer => {
const noLayersMessageLayer = new Konva.Layer({ const noLayersMessageLayer = new Konva.Layer({
id: NO_LAYERS_MESSAGE_LAYER_ID, id: NO_LAYERS_MESSAGE_LAYER_ID,
@ -901,7 +984,14 @@ const createNoLayersMessageLayer = (stage: Konva.Stage): Konva.Layer => {
return noLayersMessageLayer; return noLayersMessageLayer;
}; };
const renderNoLayersMessage = (stage: Konva.Stage, layerCount: number, width: number, height: number) => { /**
* Renders the "no layers" message when there are no layers to render
* @param stage The konva stage
* @param layerCount The current number of layers
* @param width The target width of the text
* @param height The target height of the text
*/
const renderNoLayersMessage = (stage: Konva.Stage, layerCount: number, width: number, height: number): void => {
const noLayersMessageLayer = const noLayersMessageLayer =
stage.findOne<Konva.Layer>(`#${NO_LAYERS_MESSAGE_LAYER_ID}`) ?? createNoLayersMessageLayer(stage); stage.findOne<Konva.Layer>(`#${NO_LAYERS_MESSAGE_LAYER_ID}`) ?? createNoLayersMessageLayer(stage);
if (layerCount === 0) { if (layerCount === 0) {
@ -942,7 +1032,7 @@ export const debouncedRenderers = {
* This is useful for edge maps and other masks, to make the black areas transparent. * This is useful for edge maps and other masks, to make the black areas transparent.
* @param imageData The image data to apply the filter to * @param imageData The image data to apply the filter to
*/ */
const LightnessToAlphaFilter = (imageData: ImageData) => { const LightnessToAlphaFilter = (imageData: ImageData): void => {
const len = imageData.data.length / 4; const len = imageData.data.length / 4;
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
const r = imageData.data[i * 4 + 0] as number; const r = imageData.data[i * 4 + 0] as number;