From 1bce156de1d2e563260e554637bc468a1639f733 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 6 Jun 2024 09:26:29 +1000 Subject: [PATCH] feat(ui): raster layer reset, object group util --- .../components/LayerCommon/LayerMenu.tsx | 5 ++- .../features/controlLayers/konva/naming.ts | 22 ++++++---- .../controlLayers/konva/renderers/objects.ts | 36 ++++++++++++---- .../konva/renderers/rasterLayer.ts | 41 ++++++++++--------- .../controlLayers/konva/renderers/rgLayer.ts | 35 ++++++++-------- .../src/features/controlLayers/konva/util.ts | 34 ++++++++++----- .../controlLayers/store/controlLayersSlice.ts | 6 +++ 7 files changed, 116 insertions(+), 63 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx index 0a3b52a3ff..aabad5ed63 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx @@ -29,6 +29,9 @@ export const LayerMenu = memo(({ layerId }: Props) => { layerType === 'raster_layer' ); }, [layerType]); + const shouldShowResetAction = useMemo(() => { + return layerType === 'regional_guidance_layer' || layerType === 'raster_layer'; + }, [layerType]); return ( @@ -52,7 +55,7 @@ export const LayerMenu = memo(({ layerId }: Props) => { )} - {layerType === 'regional_guidance_layer' && ( + {shouldShowResetAction && ( }> {t('accessibility.reset')} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts index f8175c9655..19c8a3332b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts @@ -14,21 +14,27 @@ export const BACKGROUND_RECT_ID = 'background_layer.rect'; export const NO_LAYERS_MESSAGE_LAYER_ID = 'no_layers_message'; // Names for Konva layers and objects (comparable to CSS classes) +export const LAYER_BBOX_NAME = 'layer.bbox'; +export const COMPOSITING_RECT_NAME = 'compositing-rect'; + export const CA_LAYER_NAME = 'control_adapter_layer'; export const CA_LAYER_IMAGE_NAME = 'control_adapter_layer.image'; -export const RG_LAYER_NAME = 'regional_guidance_layer'; -export const RG_LAYER_LINE_NAME = 'regional_guidance_layer.line'; -export const RG_LAYER_OBJECT_GROUP_NAME = 'regional_guidance_layer.object_group'; -export const RG_LAYER_RECT_NAME = 'regional_guidance_layer.rect'; + export const INITIAL_IMAGE_LAYER_ID = 'singleton_initial_image_layer'; export const INITIAL_IMAGE_LAYER_NAME = 'initial_image_layer'; export const INITIAL_IMAGE_LAYER_IMAGE_NAME = 'initial_image_layer.image'; -export const LAYER_BBOX_NAME = 'layer.bbox'; -export const COMPOSITING_RECT_NAME = 'compositing-rect'; + +export const RG_LAYER_NAME = 'regional_guidance_layer'; +export const RG_LAYER_OBJECT_GROUP_NAME = 'regional_guidance_layer.object_group'; +export const RG_LAYER_BRUSH_LINE_NAME = 'regional_guidance_layer.brush_line'; +export const RG_LAYER_ERASER_LINE_NAME = 'regional_guidance_layer.eraser_line'; +export const RG_LAYER_RECT_SHAPE_NAME = 'regional_guidance_layer.rect_shape'; + export const RASTER_LAYER_NAME = 'raster_layer'; -export const RASTER_LAYER_LINE_NAME = 'raster_layer.line'; export const RASTER_LAYER_OBJECT_GROUP_NAME = 'raster_layer.object_group'; -export const RASTER_LAYER_RECT_NAME = 'raster_layer.rect'; +export const RASTER_LAYER_BRUSH_LINE_NAME = 'raster_layer.brush_line'; +export const RASTER_LAYER_ERASER_LINE_NAME = 'raster_layer.eraser_line'; +export const RASTER_LAYER_RECT_SHAPE_NAME = 'raster_layer.rect_shape'; // Getters for non-singleton layer and object IDs export const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`; 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 50d23bd63c..b5a1f516ed 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -1,8 +1,9 @@ import { rgbaColorToString } from 'features/canvas/util/colorToString'; -import { RG_LAYER_LINE_NAME, RG_LAYER_RECT_NAME } from 'features/controlLayers/konva/naming'; +import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import type { BrushLine, EraserLine, RectShape } from 'features/controlLayers/store/types'; import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; import Konva from 'konva'; +import { v4 as uuidv4 } from 'uuid'; /** * Utilities to create various konva objects from layer state. These are used by both the raster and regional guidance @@ -13,12 +14,13 @@ import Konva from 'konva'; * Creates a konva line for a brush line. * @param brushLine The brush line state * @param layerObjectGroup The konva layer's object group to add the line to + * @param name The konva name for the line */ -export const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group): Konva.Line => { +export const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group, name: string): Konva.Line => { const konvaLine = new Konva.Line({ id: brushLine.id, key: brushLine.id, - name: RG_LAYER_LINE_NAME, + name, strokeWidth: brushLine.strokeWidth, tension: 0, lineCap: 'round', @@ -36,12 +38,13 @@ export const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Gr * Creates a konva line for a eraser line. * @param eraserLine The eraser line state * @param layerObjectGroup The konva layer's object group to add the line to + * @param name The konva name for the line */ -export const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Group): Konva.Line => { +export const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Group, name: string): Konva.Line => { const konvaLine = new Konva.Line({ id: eraserLine.id, key: eraserLine.id, - name: RG_LAYER_LINE_NAME, + name, strokeWidth: eraserLine.strokeWidth, tension: 0, lineCap: 'round', @@ -58,13 +61,14 @@ export const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva /** * Creates a konva rect for a rect shape. * @param rectShape The rect shape state - * @param layerObjectGroup The konva layer's object group to add the line to + * @param layerObjectGroup The konva layer's object group to add the rect to + * @param name The konva name for the rect */ -export const createRectShape = (rectShape: RectShape, layerObjectGroup: Konva.Group): Konva.Rect => { +export const createRectShape = (rectShape: RectShape, layerObjectGroup: Konva.Group, name: string): Konva.Rect => { const konvaRect = new Konva.Rect({ id: rectShape.id, key: rectShape.id, - name: RG_LAYER_RECT_NAME, + name, x: rectShape.x, y: rectShape.y, width: rectShape.width, @@ -75,3 +79,19 @@ export const createRectShape = (rectShape: RectShape, layerObjectGroup: Konva.Gr layerObjectGroup.add(konvaRect); return konvaRect; }; + +/** + * Creates a konva group for a layer's objects. + * @param konvaLayer The konva layer to add the object group to + * @param name The konva name for the group + * @returns + */ +export const createObjectGroup = (konvaLayer: Konva.Layer, name: string): Konva.Group => { + const konvaObjectGroup = new Konva.Group({ + id: getObjectGroupId(konvaLayer.id(), uuidv4()), + name, + listening: false, + }); + konvaLayer.add(konvaObjectGroup); + return konvaObjectGroup; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts index 81251b5f2b..1c23d48036 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts @@ -1,14 +1,19 @@ import { - getObjectGroupId, + RASTER_LAYER_BRUSH_LINE_NAME, + RASTER_LAYER_ERASER_LINE_NAME, RASTER_LAYER_NAME, RASTER_LAYER_OBJECT_GROUP_NAME, + RASTER_LAYER_RECT_SHAPE_NAME, } from 'features/controlLayers/konva/naming'; -import { createBrushLine, createEraserLine, createRectShape } from 'features/controlLayers/konva/renderers/objects'; -import { getScaledFlooredCursorPosition, mapId } from 'features/controlLayers/konva/util'; +import { + createBrushLine, + createEraserLine, + createObjectGroup, + createRectShape, +} from 'features/controlLayers/konva/renderers/objects'; +import { getScaledFlooredCursorPosition, mapId, selectRasterObjects } from 'features/controlLayers/konva/util'; import type { RasterLayer, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; -import { assert } from 'tsafe'; -import { v4 as uuidv4 } from 'uuid'; /** * Logic for creating and rendering raster layers. @@ -59,14 +64,6 @@ const createRasterLayer = ( return pos; }); - // The object group holds all of the layer's objects (e.g. lines and rects) - const konvaObjectGroup = new Konva.Group({ - id: getObjectGroupId(layerState.id, uuidv4()), - name: RASTER_LAYER_OBJECT_GROUP_NAME, - listening: false, - }); - konvaLayer.add(konvaObjectGroup); - stage.add(konvaLayer); return konvaLayer; @@ -95,12 +92,15 @@ export const renderRasterLayer = ( y: Math.floor(layerState.y), }); - const konvaObjectGroup = konvaLayer.findOne(`.${RASTER_LAYER_OBJECT_GROUP_NAME}`); - assert(konvaObjectGroup, `Object group not found for layer ${layerState.id}`); + const konvaObjectGroup = + konvaLayer.findOne(`.${RASTER_LAYER_OBJECT_GROUP_NAME}`) ?? + createObjectGroup(konvaLayer, RASTER_LAYER_OBJECT_GROUP_NAME); const objectIds = layerState.objects.map(mapId); // Destroy any objects that are no longer in the redux state - for (const objectNode of konvaObjectGroup.getChildren()) { + // TODO(psyche): `konvaObjectGroup.getChildren()` seems to return a stale array of children, but find is never stale. + // Should report upstream + for (const objectNode of konvaObjectGroup.find(selectRasterObjects)) { if (!objectIds.includes(objectNode.id())) { objectNode.destroy(); } @@ -108,20 +108,23 @@ export const renderRasterLayer = ( for (const obj of layerState.objects) { if (obj.type === 'brush_line') { - const konvaBrushLine = stage.findOne(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup); + const konvaBrushLine = + stage.findOne(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup, RASTER_LAYER_BRUSH_LINE_NAME); // Only update the points if they have changed. if (konvaBrushLine.points().length !== obj.points.length) { konvaBrushLine.points(obj.points); } } else if (obj.type === 'eraser_line') { - const konvaEraserLine = stage.findOne(`#${obj.id}`) ?? createEraserLine(obj, konvaObjectGroup); + const konvaEraserLine = + stage.findOne(`#${obj.id}`) ?? + createEraserLine(obj, konvaObjectGroup, RASTER_LAYER_ERASER_LINE_NAME); // Only update the points if they have changed. if (konvaEraserLine.points().length !== obj.points.length) { konvaEraserLine.points(obj.points); } } else if (obj.type === 'rect_shape') { if (!stage.findOne(`#${obj.id}`)) { - createRectShape(obj, konvaObjectGroup); + createRectShape(obj, konvaObjectGroup, RASTER_LAYER_RECT_SHAPE_NAME); } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts index 471f23ac5a..4321a85c01 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts @@ -1,17 +1,22 @@ import { rgbColorToString } from 'features/canvas/util/colorToString'; import { COMPOSITING_RECT_NAME, - getObjectGroupId, + RG_LAYER_BRUSH_LINE_NAME, + RG_LAYER_ERASER_LINE_NAME, RG_LAYER_NAME, RG_LAYER_OBJECT_GROUP_NAME, + RG_LAYER_RECT_SHAPE_NAME, } from 'features/controlLayers/konva/naming'; import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox'; -import { createBrushLine, createEraserLine, createRectShape } from 'features/controlLayers/konva/renderers/objects'; +import { + createBrushLine, + createEraserLine, + createObjectGroup, + createRectShape, +} from 'features/controlLayers/konva/renderers/objects'; import { getScaledFlooredCursorPosition, mapId, selectVectorMaskObjects } from 'features/controlLayers/konva/util'; import type { RegionalGuidanceLayer, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; -import { assert } from 'tsafe'; -import { v4 as uuidv4 } from 'uuid'; /** * Logic for creating and rendering regional guidance layers. @@ -75,14 +80,6 @@ const createRGLayer = ( return pos; }); - // The object group holds all of the layer's objects (e.g. lines and rects) - const konvaObjectGroup = new Konva.Group({ - id: getObjectGroupId(layerState.id, uuidv4()), - name: RG_LAYER_OBJECT_GROUP_NAME, - listening: false, - }); - konvaLayer.add(konvaObjectGroup); - stage.add(konvaLayer); return konvaLayer; @@ -116,8 +113,9 @@ export const renderRGLayer = ( // Convert the color to a string, stripping the alpha - the object group will handle opacity. const rgbColor = rgbColorToString(layerState.previewColor); - const konvaObjectGroup = konvaLayer.findOne(`.${RG_LAYER_OBJECT_GROUP_NAME}`); - assert(konvaObjectGroup, `Object group not found for layer ${layerState.id}`); + const konvaObjectGroup = + konvaLayer.findOne(`.${RG_LAYER_OBJECT_GROUP_NAME}`) ?? + createObjectGroup(konvaLayer, RG_LAYER_OBJECT_GROUP_NAME); // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. let groupNeedsCache = false; @@ -133,7 +131,8 @@ export const renderRGLayer = ( for (const obj of layerState.objects) { if (obj.type === 'brush_line') { - const konvaBrushLine = stage.findOne(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup); + const konvaBrushLine = + stage.findOne(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup, 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. @@ -147,7 +146,8 @@ export const renderRGLayer = ( groupNeedsCache = true; } } else if (obj.type === 'eraser_line') { - const konvaEraserLine = stage.findOne(`#${obj.id}`) ?? createEraserLine(obj, konvaObjectGroup); + const konvaEraserLine = + stage.findOne(`#${obj.id}`) ?? createEraserLine(obj, konvaObjectGroup, 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. @@ -161,7 +161,8 @@ export const renderRGLayer = ( groupNeedsCache = true; } } else if (obj.type === 'rect_shape') { - const konvaRectShape = stage.findOne(`#${obj.id}`) ?? createRectShape(obj, konvaObjectGroup); + const konvaRectShape = + stage.findOne(`#${obj.id}`) ?? createRectShape(obj, konvaObjectGroup, RG_LAYER_RECT_SHAPE_NAME); // Only update the color if it has changed. if (konvaRectShape.fill() !== rgbColor) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 2eed6a663b..0061889a5e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,10 +1,14 @@ import { CA_LAYER_NAME, INITIAL_IMAGE_LAYER_NAME, + RASTER_LAYER_BRUSH_LINE_NAME, + RASTER_LAYER_ERASER_LINE_NAME, RASTER_LAYER_NAME, - RG_LAYER_LINE_NAME, + RASTER_LAYER_RECT_SHAPE_NAME, + RG_LAYER_BRUSH_LINE_NAME, + RG_LAYER_ERASER_LINE_NAME, RG_LAYER_NAME, - RG_LAYER_RECT_NAME, + RG_LAYER_RECT_SHAPE_NAME, } from 'features/controlLayers/konva/naming'; import type Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; @@ -89,17 +93,27 @@ export const mapId = (object: { id: string }): string => object.id; * Konva selection callback to select all renderable layers. This includes RG, CA II and Raster layers. * This can be provided to the `find` or `findOne` konva node methods. */ -export const selectRenderableLayers = (n: Konva.Node): boolean => - n.name() === RG_LAYER_NAME || - n.name() === CA_LAYER_NAME || - n.name() === INITIAL_IMAGE_LAYER_NAME || - n.name() === RASTER_LAYER_NAME; +export const selectRenderableLayers = (node: Konva.Node): boolean => + node.name() === RG_LAYER_NAME || + node.name() === CA_LAYER_NAME || + node.name() === INITIAL_IMAGE_LAYER_NAME || + node.name() === RASTER_LAYER_NAME; /** * Konva selection callback to select RG mask objects. This includes lines and rects. * This can be provided to the `find` or `findOne` konva node methods. */ -export const selectVectorMaskObjects = (node: Konva.Node): boolean => { - return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME; -}; +export const selectVectorMaskObjects = (node: Konva.Node): boolean => + node.name() === RG_LAYER_BRUSH_LINE_NAME || + node.name() === RG_LAYER_ERASER_LINE_NAME || + node.name() === RG_LAYER_RECT_SHAPE_NAME; + +/** + * Konva selection callback to select raster layer objects. This includes lines and rects. + * This can be provided to the `find` or `findOne` konva node methods. + */ +export const selectRasterObjects = (node: Konva.Node): boolean => + node.name() === RASTER_LAYER_BRUSH_LINE_NAME || + node.name() === RASTER_LAYER_ERASER_LINE_NAME || + node.name() === RASTER_LAYER_RECT_SHAPE_NAME; //#endregion diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index b2fc8b0754..a1709cac6d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -177,6 +177,12 @@ export const controlLayersSlice = createSlice({ layer.bboxNeedsUpdate = false; layer.uploadedMaskImage = null; } + if (isRasterLayer(layer)) { + layer.isEnabled = true; + layer.objects = []; + layer.bbox = null; + layer.bboxNeedsUpdate = false; + } }, layerDeleted: (state, action: PayloadAction) => { state.layers = state.layers.filter((l) => l.id !== action.payload);