diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx index 517385f0d3..4c218a87fd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx @@ -1,4 +1,4 @@ -import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger, Tooltip } from '@invoke-ai/ui-library'; +import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIColorPicker from 'common/components/IAIColorPicker'; import { rgbaColorToString } from 'features/canvas/util/colorToString'; @@ -20,21 +20,17 @@ export const BrushColorPicker = memo(() => { return ( - - - - - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx index a34250c29f..731f9a1c77 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx @@ -28,7 +28,7 @@ export const BrushSize = memo(() => { [dispatch] ); return ( - + {t('controlLayers.brushSize')} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 55025d40f2..37f46b51cd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -1,31 +1,40 @@ /* eslint-disable i18next/no-literal-string */ import { Flex } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { BrushColorPicker } from 'features/controlLayers/components/BrushColorPicker'; import { BrushSize } from 'features/controlLayers/components/BrushSize'; import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover'; import { ToolChooser } from 'features/controlLayers/components/ToolChooser'; import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup'; +import { $tool } from 'features/controlLayers/store/controlLayersSlice'; import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; export const ControlLayersToolbar = memo(() => { + const tool = useStore($tool); + const withBrushSize = useMemo(() => { + return tool === 'brush' || tool === 'eraser'; + }, [tool]); + const withBrushColor = useMemo(() => { + return tool === 'brush'; + }, [tool]); return ( + - - - - - - + + {withBrushSize && } + {withBrushColor && } + + diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts index 19c8a3332b..b5ceefbf14 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts @@ -35,13 +35,15 @@ export const RASTER_LAYER_OBJECT_GROUP_NAME = 'raster_layer.object_group'; 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'; +export const RASTER_LAYER_IMAGE_NAME = 'raster_layer.image'; // Getters for non-singleton layer and object IDs export const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`; export const getRasterLayerId = (layerId: string) => `${RASTER_LAYER_NAME}_${layerId}`; export const getBrushLineId = (layerId: string, lineId: string) => `${layerId}.brush_line_${lineId}`; export const getEraserLineId = (layerId: string, lineId: string) => `${layerId}.eraser_line_${lineId}`; -export const getRectId = (layerId: string, lineId: string) => `${layerId}.rect_${lineId}`; +export const getRectShapeId = (layerId: string, lineId: string) => `${layerId}.rect_${lineId}`; +export const getImageObjectId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`; export const getObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`; export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`; export const getCALayerId = (layerId: string) => `control_adapter_layer_${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 b5a1f516ed..e5e54c9593 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 { getObjectGroupId } from 'features/controlLayers/konva/naming'; -import type { BrushLine, EraserLine, RectShape } from 'features/controlLayers/store/types'; +import type { BrushLine, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types'; import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; import Konva from 'konva'; +import { getImageDTO } from 'services/api/endpoints/images'; import { v4 as uuidv4 } from 'uuid'; /** @@ -80,6 +81,34 @@ export const createRectShape = (rectShape: RectShape, layerObjectGroup: Konva.Gr return konvaRect; }; +export const createImageObject = async ( + imageObject: ImageObject, + layerObjectGroup: Konva.Group, + name: string +): Promise => { + const imageDTO = await getImageDTO(imageObject.image.name); + if (!imageDTO) { + return null; + } + return new Promise((resolve) => { + const imageEl = new Image(); + imageEl.onload = () => { + const konvaImage = new Konva.Image({ + id: imageObject.id, + name, + listening: false, + image: imageEl, + }); + layerObjectGroup.add(konvaImage); + resolve(konvaImage); + }; + imageEl.onerror = () => { + resolve(null); + }; + imageEl.id = imageObject.id; + imageEl.src = imageDTO.image_url; + }); +}; /** * Creates a konva group for a layer's objects. * @param konvaLayer The konva layer to add the object group to 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 1c23d48036..ca74fee718 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts @@ -1,6 +1,7 @@ import { RASTER_LAYER_BRUSH_LINE_NAME, RASTER_LAYER_ERASER_LINE_NAME, + RASTER_LAYER_IMAGE_NAME, RASTER_LAYER_NAME, RASTER_LAYER_OBJECT_GROUP_NAME, RASTER_LAYER_RECT_SHAPE_NAME, @@ -8,12 +9,14 @@ import { import { createBrushLine, createEraserLine, + createImageObject, 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'; /** * Logic for creating and rendering raster layers. @@ -76,12 +79,12 @@ const createRasterLayer = ( * @param tool The current tool * @param onLayerPosChanged Callback for when the layer's position changes */ -export const renderRasterLayer = ( +export const renderRasterLayer = async ( stage: Konva.Stage, layerState: RasterLayer, tool: Tool, onLayerPosChanged?: (layerId: string, x: number, y: number) => void -): void => { +) => { const konvaLayer = stage.findOne(`#${layerState.id}`) ?? createRasterLayer(stage, layerState, onLayerPosChanged); @@ -106,26 +109,38 @@ export const renderRasterLayer = ( } } - for (const obj of layerState.objects) { + for (let i = 0; i < layerState.objects.length; i++) { + const obj = layerState.objects[i]; + assert(obj); + const zIndex = layerState.objects.length - i; if (obj.type === 'brush_line') { const konvaBrushLine = - stage.findOne(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup, RASTER_LAYER_BRUSH_LINE_NAME); + konvaObjectGroup.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); } + konvaBrushLine.zIndex(zIndex); } else if (obj.type === 'eraser_line') { const konvaEraserLine = - stage.findOne(`#${obj.id}`) ?? + konvaObjectGroup.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); } + konvaEraserLine.zIndex(zIndex); } else if (obj.type === 'rect_shape') { - if (!stage.findOne(`#${obj.id}`)) { + const konvaRect = + konvaObjectGroup.findOne(`#${obj.id}`) ?? createRectShape(obj, konvaObjectGroup, RASTER_LAYER_RECT_SHAPE_NAME); - } + konvaRect.zIndex(zIndex); + } else if (obj.type === 'image') { + const konvaImage = + konvaObjectGroup.findOne(`#${obj.id}`) ?? + (await createImageObject(obj, konvaObjectGroup, RASTER_LAYER_IMAGE_NAME)); + konvaImage?.zIndex(zIndex); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 0061889a5e..1143940bfe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -3,6 +3,7 @@ import { INITIAL_IMAGE_LAYER_NAME, RASTER_LAYER_BRUSH_LINE_NAME, RASTER_LAYER_ERASER_LINE_NAME, + RASTER_LAYER_IMAGE_NAME, RASTER_LAYER_NAME, RASTER_LAYER_RECT_SHAPE_NAME, RG_LAYER_BRUSH_LINE_NAME, @@ -115,5 +116,6 @@ export const selectVectorMaskObjects = (node: Konva.Node): boolean => 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; + node.name() === RASTER_LAYER_RECT_SHAPE_NAME || + node.name() === RASTER_LAYER_IMAGE_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 4c2c98fe40..345752c93a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -11,7 +11,7 @@ import { getImageObjectId, getIPALayerId, getRasterLayerId, - getRectId, + getRectShapeId, getRGLayerId, INITIAL_IMAGE_LAYER_ID, } from 'features/controlLayers/konva/naming';